koprogo_api/application/use_cases/
resolution_use_cases.rs

1use crate::application::ports::{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}
12
13impl ResolutionUseCases {
14    pub fn new(
15        resolution_repository: Arc<dyn ResolutionRepository>,
16        vote_repository: Arc<dyn VoteRepository>,
17    ) -> Self {
18        Self {
19            resolution_repository,
20            vote_repository,
21        }
22    }
23
24    /// Create a new resolution for a meeting
25    pub async fn create_resolution(
26        &self,
27        meeting_id: Uuid,
28        title: String,
29        description: String,
30        resolution_type: ResolutionType,
31        majority_required: MajorityType,
32    ) -> Result<Resolution, String> {
33        let resolution = Resolution::new(
34            meeting_id,
35            title,
36            description,
37            resolution_type,
38            majority_required,
39        )?;
40
41        self.resolution_repository.create(&resolution).await
42    }
43
44    /// Get a resolution by ID
45    pub async fn get_resolution(&self, id: Uuid) -> Result<Option<Resolution>, String> {
46        self.resolution_repository.find_by_id(id).await
47    }
48
49    /// Get all resolutions for a meeting
50    pub async fn get_meeting_resolutions(
51        &self,
52        meeting_id: Uuid,
53    ) -> Result<Vec<Resolution>, String> {
54        self.resolution_repository
55            .find_by_meeting_id(meeting_id)
56            .await
57    }
58
59    /// Get resolutions by status
60    pub async fn get_resolutions_by_status(
61        &self,
62        status: ResolutionStatus,
63    ) -> Result<Vec<Resolution>, String> {
64        self.resolution_repository.find_by_status(status).await
65    }
66
67    /// Update a resolution (only allowed if status is Pending)
68    pub async fn update_resolution(&self, resolution: &Resolution) -> Result<Resolution, String> {
69        if resolution.status != ResolutionStatus::Pending {
70            return Err("Cannot update a resolution that is not pending".to_string());
71        }
72
73        self.resolution_repository.update(resolution).await
74    }
75
76    /// Delete a resolution (only allowed if no votes have been cast)
77    pub async fn delete_resolution(&self, id: Uuid) -> Result<bool, String> {
78        // Check if any votes exist
79        let votes = self.vote_repository.find_by_resolution_id(id).await?;
80        if !votes.is_empty() {
81            return Err("Cannot delete a resolution with existing votes".to_string());
82        }
83
84        self.resolution_repository.delete(id).await
85    }
86
87    /// Cast a vote on a resolution
88    pub async fn cast_vote(
89        &self,
90        resolution_id: Uuid,
91        owner_id: Uuid,
92        unit_id: Uuid,
93        vote_choice: VoteChoice,
94        voting_power: f64,
95        proxy_owner_id: Option<Uuid>,
96    ) -> Result<Vote, String> {
97        // Check if resolution exists and is pending
98        let resolution = self
99            .resolution_repository
100            .find_by_id(resolution_id)
101            .await?
102            .ok_or_else(|| "Resolution not found".to_string())?;
103
104        if resolution.status != ResolutionStatus::Pending {
105            return Err("Cannot vote on a resolution that is not pending".to_string());
106        }
107
108        // Check if unit has already voted
109        if self
110            .vote_repository
111            .has_voted(resolution_id, unit_id)
112            .await?
113        {
114            return Err("This unit has already voted on this resolution".to_string());
115        }
116
117        // Create and save the vote
118        let vote = Vote::new(
119            resolution_id,
120            owner_id,
121            unit_id,
122            vote_choice.clone(),
123            voting_power,
124            proxy_owner_id,
125        )?;
126
127        let created_vote = self.vote_repository.create(&vote).await?;
128
129        // Update vote counts on the resolution
130        self.update_resolution_vote_counts(resolution_id).await?;
131
132        Ok(created_vote)
133    }
134
135    /// Change a vote (if allowed by business rules)
136    pub async fn change_vote(&self, vote_id: Uuid, new_choice: VoteChoice) -> Result<Vote, String> {
137        let mut vote = self
138            .vote_repository
139            .find_by_id(vote_id)
140            .await?
141            .ok_or_else(|| "Vote not found".to_string())?;
142
143        // Check if resolution is still pending
144        let resolution = self
145            .resolution_repository
146            .find_by_id(vote.resolution_id)
147            .await?
148            .ok_or_else(|| "Resolution not found".to_string())?;
149
150        if resolution.status != ResolutionStatus::Pending {
151            return Err("Cannot change vote on a closed resolution".to_string());
152        }
153
154        // Update the vote
155        vote.change_vote(new_choice)?;
156        let updated_vote = self.vote_repository.update(&vote).await?;
157
158        // Recalculate vote counts
159        self.update_resolution_vote_counts(vote.resolution_id)
160            .await?;
161
162        Ok(updated_vote)
163    }
164
165    /// Get all votes for a resolution
166    pub async fn get_resolution_votes(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
167        self.vote_repository
168            .find_by_resolution_id(resolution_id)
169            .await
170    }
171
172    /// Get all votes by an owner
173    pub async fn get_owner_votes(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
174        self.vote_repository.find_by_owner_id(owner_id).await
175    }
176
177    /// Close voting on a resolution and calculate final result
178    pub async fn close_voting(
179        &self,
180        resolution_id: Uuid,
181        total_voting_power: f64,
182    ) -> Result<Resolution, String> {
183        let mut resolution = self
184            .resolution_repository
185            .find_by_id(resolution_id)
186            .await?
187            .ok_or_else(|| "Resolution not found".to_string())?;
188
189        if resolution.status != ResolutionStatus::Pending {
190            return Err("Resolution voting is already closed".to_string());
191        }
192
193        // Calculate final result
194        resolution.close_voting(total_voting_power)?;
195
196        // Update resolution with final status
197        self.resolution_repository
198            .close_voting(resolution_id, resolution.status.clone())
199            .await?;
200
201        // Fetch updated resolution
202        self.resolution_repository
203            .find_by_id(resolution_id)
204            .await?
205            .ok_or_else(|| "Resolution not found after closing".to_string())
206    }
207
208    /// Get vote summary for a meeting (all resolutions with their results)
209    pub async fn get_meeting_vote_summary(
210        &self,
211        meeting_id: Uuid,
212    ) -> Result<Vec<Resolution>, String> {
213        self.resolution_repository
214            .get_meeting_vote_summary(meeting_id)
215            .await
216    }
217
218    /// Helper: Update vote counts for a resolution based on actual votes
219    async fn update_resolution_vote_counts(&self, resolution_id: Uuid) -> Result<(), String> {
220        // Get vote counts
221        let (pour_count, contre_count, abstention_count) = self
222            .vote_repository
223            .count_by_resolution_and_choice(resolution_id)
224            .await?;
225
226        // Get voting power totals
227        let (pour_power, contre_power, abstention_power) = self
228            .vote_repository
229            .sum_voting_power_by_resolution(resolution_id)
230            .await?;
231
232        // Update resolution
233        self.resolution_repository
234            .update_vote_counts(
235                resolution_id,
236                pour_count,
237                contre_count,
238                abstention_count,
239                pour_power,
240                contre_power,
241                abstention_power,
242            )
243            .await
244    }
245
246    /// Check if a unit has voted on a resolution
247    pub async fn has_unit_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
248        self.vote_repository.has_voted(resolution_id, unit_id).await
249    }
250
251    /// Get vote statistics for a resolution
252    pub async fn get_vote_statistics(&self, resolution_id: Uuid) -> Result<VoteStatistics, String> {
253        let resolution = self
254            .resolution_repository
255            .find_by_id(resolution_id)
256            .await?
257            .ok_or_else(|| "Resolution not found".to_string())?;
258
259        Ok(VoteStatistics {
260            total_votes: resolution.total_votes(),
261            vote_count_pour: resolution.vote_count_pour,
262            vote_count_contre: resolution.vote_count_contre,
263            vote_count_abstention: resolution.vote_count_abstention,
264            total_voting_power_pour: resolution.total_voting_power_pour,
265            total_voting_power_contre: resolution.total_voting_power_contre,
266            total_voting_power_abstention: resolution.total_voting_power_abstention,
267            pour_percentage: resolution.pour_percentage(),
268            contre_percentage: resolution.contre_percentage(),
269            abstention_percentage: resolution.abstention_percentage(),
270            status: resolution.status,
271        })
272    }
273}
274
275/// Vote statistics for a resolution
276#[derive(Debug, Clone)]
277pub struct VoteStatistics {
278    pub total_votes: i32,
279    pub vote_count_pour: i32,
280    pub vote_count_contre: i32,
281    pub vote_count_abstention: i32,
282    pub total_voting_power_pour: f64,
283    pub total_voting_power_contre: f64,
284    pub total_voting_power_abstention: f64,
285    pub pour_percentage: f64,
286    pub contre_percentage: f64,
287    pub abstention_percentage: f64,
288    pub status: ResolutionStatus,
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::application::ports::ResolutionRepository;
295    use crate::application::ports::VoteRepository;
296    use async_trait::async_trait;
297    use std::collections::HashMap;
298    use std::sync::Mutex;
299
300    // Mock repositories for testing
301    struct MockResolutionRepository {
302        resolutions: Mutex<HashMap<Uuid, Resolution>>,
303    }
304
305    impl MockResolutionRepository {
306        fn new() -> Self {
307            Self {
308                resolutions: Mutex::new(HashMap::new()),
309            }
310        }
311    }
312
313    #[async_trait]
314    impl ResolutionRepository for MockResolutionRepository {
315        async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
316            self.resolutions
317                .lock()
318                .unwrap()
319                .insert(resolution.id, resolution.clone());
320            Ok(resolution.clone())
321        }
322
323        async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
324            Ok(self.resolutions.lock().unwrap().get(&id).cloned())
325        }
326
327        async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
328            Ok(self
329                .resolutions
330                .lock()
331                .unwrap()
332                .values()
333                .filter(|r| r.meeting_id == meeting_id)
334                .cloned()
335                .collect())
336        }
337
338        async fn find_by_status(
339            &self,
340            status: ResolutionStatus,
341        ) -> Result<Vec<Resolution>, String> {
342            Ok(self
343                .resolutions
344                .lock()
345                .unwrap()
346                .values()
347                .filter(|r| r.status == status)
348                .cloned()
349                .collect())
350        }
351
352        async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
353            self.resolutions
354                .lock()
355                .unwrap()
356                .insert(resolution.id, resolution.clone());
357            Ok(resolution.clone())
358        }
359
360        async fn delete(&self, id: Uuid) -> Result<bool, String> {
361            Ok(self.resolutions.lock().unwrap().remove(&id).is_some())
362        }
363
364        async fn update_vote_counts(
365            &self,
366            resolution_id: Uuid,
367            vote_count_pour: i32,
368            vote_count_contre: i32,
369            vote_count_abstention: i32,
370            total_voting_power_pour: f64,
371            total_voting_power_contre: f64,
372            total_voting_power_abstention: f64,
373        ) -> Result<(), String> {
374            if let Some(resolution) = self.resolutions.lock().unwrap().get_mut(&resolution_id) {
375                resolution.vote_count_pour = vote_count_pour;
376                resolution.vote_count_contre = vote_count_contre;
377                resolution.vote_count_abstention = vote_count_abstention;
378                resolution.total_voting_power_pour = total_voting_power_pour;
379                resolution.total_voting_power_contre = total_voting_power_contre;
380                resolution.total_voting_power_abstention = total_voting_power_abstention;
381            }
382            Ok(())
383        }
384
385        async fn close_voting(
386            &self,
387            resolution_id: Uuid,
388            final_status: ResolutionStatus,
389        ) -> Result<(), String> {
390            if let Some(resolution) = self.resolutions.lock().unwrap().get_mut(&resolution_id) {
391                resolution.status = final_status;
392                resolution.voted_at = Some(chrono::Utc::now());
393            }
394            Ok(())
395        }
396
397        async fn get_meeting_vote_summary(
398            &self,
399            meeting_id: Uuid,
400        ) -> Result<Vec<Resolution>, String> {
401            self.find_by_meeting_id(meeting_id).await
402        }
403    }
404
405    struct MockVoteRepository {
406        votes: Mutex<HashMap<Uuid, Vote>>,
407    }
408
409    impl MockVoteRepository {
410        fn new() -> Self {
411            Self {
412                votes: Mutex::new(HashMap::new()),
413            }
414        }
415    }
416
417    #[async_trait]
418    impl VoteRepository for MockVoteRepository {
419        async fn create(&self, vote: &Vote) -> Result<Vote, String> {
420            self.votes.lock().unwrap().insert(vote.id, vote.clone());
421            Ok(vote.clone())
422        }
423
424        async fn find_by_id(&self, id: Uuid) -> Result<Option<Vote>, String> {
425            Ok(self.votes.lock().unwrap().get(&id).cloned())
426        }
427
428        async fn find_by_resolution_id(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
429            Ok(self
430                .votes
431                .lock()
432                .unwrap()
433                .values()
434                .filter(|v| v.resolution_id == resolution_id)
435                .cloned()
436                .collect())
437        }
438
439        async fn find_by_owner_id(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
440            Ok(self
441                .votes
442                .lock()
443                .unwrap()
444                .values()
445                .filter(|v| v.owner_id == owner_id)
446                .cloned()
447                .collect())
448        }
449
450        async fn find_by_resolution_and_unit(
451            &self,
452            resolution_id: Uuid,
453            unit_id: Uuid,
454        ) -> Result<Option<Vote>, String> {
455            Ok(self
456                .votes
457                .lock()
458                .unwrap()
459                .values()
460                .find(|v| v.resolution_id == resolution_id && v.unit_id == unit_id)
461                .cloned())
462        }
463
464        async fn has_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
465            Ok(self
466                .find_by_resolution_and_unit(resolution_id, unit_id)
467                .await?
468                .is_some())
469        }
470
471        async fn update(&self, vote: &Vote) -> Result<Vote, String> {
472            self.votes.lock().unwrap().insert(vote.id, vote.clone());
473            Ok(vote.clone())
474        }
475
476        async fn delete(&self, id: Uuid) -> Result<bool, String> {
477            Ok(self.votes.lock().unwrap().remove(&id).is_some())
478        }
479
480        async fn count_by_resolution_and_choice(
481            &self,
482            resolution_id: Uuid,
483        ) -> Result<(i32, i32, i32), String> {
484            let votes = self.find_by_resolution_id(resolution_id).await?;
485            let pour = votes
486                .iter()
487                .filter(|v| v.vote_choice == VoteChoice::Pour)
488                .count() as i32;
489            let contre = votes
490                .iter()
491                .filter(|v| v.vote_choice == VoteChoice::Contre)
492                .count() as i32;
493            let abstention = votes
494                .iter()
495                .filter(|v| v.vote_choice == VoteChoice::Abstention)
496                .count() as i32;
497            Ok((pour, contre, abstention))
498        }
499
500        async fn sum_voting_power_by_resolution(
501            &self,
502            resolution_id: Uuid,
503        ) -> Result<(f64, f64, f64), String> {
504            let votes = self.find_by_resolution_id(resolution_id).await?;
505            let pour: f64 = votes
506                .iter()
507                .filter(|v| v.vote_choice == VoteChoice::Pour)
508                .map(|v| v.voting_power)
509                .sum();
510            let contre: f64 = votes
511                .iter()
512                .filter(|v| v.vote_choice == VoteChoice::Contre)
513                .map(|v| v.voting_power)
514                .sum();
515            let abstention: f64 = votes
516                .iter()
517                .filter(|v| v.vote_choice == VoteChoice::Abstention)
518                .map(|v| v.voting_power)
519                .sum();
520            Ok((pour, contre, abstention))
521        }
522    }
523
524    #[tokio::test]
525    async fn test_create_resolution() {
526        let resolution_repo = Arc::new(MockResolutionRepository::new());
527        let vote_repo = Arc::new(MockVoteRepository::new());
528        let use_cases = ResolutionUseCases::new(resolution_repo.clone(), vote_repo);
529
530        let meeting_id = Uuid::new_v4();
531        let result = use_cases
532            .create_resolution(
533                meeting_id,
534                "Test Resolution".to_string(),
535                "Description".to_string(),
536                ResolutionType::Ordinary,
537                MajorityType::Simple,
538            )
539            .await;
540
541        assert!(result.is_ok());
542        let resolution = result.unwrap();
543        assert_eq!(resolution.title, "Test Resolution");
544        assert_eq!(resolution.status, ResolutionStatus::Pending);
545    }
546
547    #[tokio::test]
548    async fn test_cast_vote_updates_counts() {
549        let resolution_repo = Arc::new(MockResolutionRepository::new());
550        let vote_repo = Arc::new(MockVoteRepository::new());
551        let use_cases = ResolutionUseCases::new(resolution_repo.clone(), vote_repo.clone());
552
553        // Create a resolution
554        let meeting_id = Uuid::new_v4();
555        let resolution = use_cases
556            .create_resolution(
557                meeting_id,
558                "Test Resolution".to_string(),
559                "Description".to_string(),
560                ResolutionType::Ordinary,
561                MajorityType::Simple,
562            )
563            .await
564            .unwrap();
565
566        // Cast a vote
567        let owner_id = Uuid::new_v4();
568        let unit_id = Uuid::new_v4();
569        let result = use_cases
570            .cast_vote(
571                resolution.id,
572                owner_id,
573                unit_id,
574                VoteChoice::Pour,
575                100.0,
576                None,
577            )
578            .await;
579
580        assert!(result.is_ok());
581
582        // Check that vote counts were updated
583        let updated_resolution = use_cases
584            .get_resolution(resolution.id)
585            .await
586            .unwrap()
587            .unwrap();
588        assert_eq!(updated_resolution.vote_count_pour, 1);
589        assert_eq!(updated_resolution.total_voting_power_pour, 100.0);
590    }
591
592    #[tokio::test]
593    async fn test_cannot_vote_twice() {
594        let resolution_repo = Arc::new(MockResolutionRepository::new());
595        let vote_repo = Arc::new(MockVoteRepository::new());
596        let use_cases = ResolutionUseCases::new(resolution_repo.clone(), vote_repo);
597
598        // Create resolution
599        let meeting_id = Uuid::new_v4();
600        let resolution = use_cases
601            .create_resolution(
602                meeting_id,
603                "Test".to_string(),
604                "Desc".to_string(),
605                ResolutionType::Ordinary,
606                MajorityType::Simple,
607            )
608            .await
609            .unwrap();
610
611        let owner_id = Uuid::new_v4();
612        let unit_id = Uuid::new_v4();
613
614        // First vote succeeds
615        let result1 = use_cases
616            .cast_vote(
617                resolution.id,
618                owner_id,
619                unit_id,
620                VoteChoice::Pour,
621                100.0,
622                None,
623            )
624            .await;
625        assert!(result1.is_ok());
626
627        // Second vote from same unit fails
628        let result2 = use_cases
629            .cast_vote(
630                resolution.id,
631                owner_id,
632                unit_id,
633                VoteChoice::Contre,
634                100.0,
635                None,
636            )
637            .await;
638        assert!(result2.is_err());
639        assert!(result2.unwrap_err().contains("already voted"));
640    }
641}