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 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 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 meeting.check_quorum_for_voting()?;
48
49 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 pub async fn get_resolution(&self, id: Uuid) -> Result<Option<Resolution>, String> {
76 self.resolution_repository.find_by_id(id).await
77 }
78
79 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 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 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 pub async fn delete_resolution(&self, id: Uuid) -> Result<bool, String> {
108 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 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 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 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 if let Some(proxy_id) = proxy_owner_id {
150 self.validate_proxy_limit(resolution_id, proxy_id, voting_power)
151 .await?;
152 }
153
154 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 self.update_resolution_vote_counts(resolution_id).await?;
168
169 Ok(created_vote)
170 }
171
172 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 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 vote.change_vote(new_choice)?;
193 let updated_vote = self.vote_repository.update(&vote).await?;
194
195 self.update_resolution_vote_counts(vote.resolution_id)
197 .await?;
198
199 Ok(updated_vote)
200 }
201
202 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 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 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 resolution.close_voting(total_voting_power)?;
232
233 self.resolution_repository
235 .close_voting(resolution_id, resolution.status.clone())
236 .await?;
237
238 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 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 async fn update_resolution_vote_counts(&self, resolution_id: Uuid) -> Result<(), String> {
257 let (pour_count, contre_count, abstention_count) = self
259 .vote_repository
260 .count_by_resolution_and_choice(resolution_id)
261 .await?;
262
263 let (pour_power, contre_power, abstention_power) = self
265 .vote_repository
266 .sum_voting_power_by_resolution(resolution_id)
267 .await?;
268
269 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 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 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 let total_proxy_power = existing_power + new_voting_power;
309
310 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 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; if total_all_votes > 0.0 && (total_proxy_power / total_all_votes) < 0.10 {
328 return Ok(()); }
330
331 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 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#[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 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 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 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 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 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 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 let org_id = Uuid::new_v4();
800 let building_id = Uuid::new_v4();
801 let meeting_id = Uuid::new_v4();
802
803 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 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 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 let org_id = Uuid::new_v4();
866 let building_id = Uuid::new_v4();
867 let meeting_id = Uuid::new_v4();
868
869 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 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 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 #[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 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 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, 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 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 #[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 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 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, None,
1069 )
1070 .await;
1071 }
1072
1073 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, 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}