1use chrono::{DateTime, Utc};
2use rust_decimal::Decimal;
3use rust_decimal_macros::dec;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum ResolutionType {
11 Ordinary, Extraordinary, }
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
17#[serde(rename_all = "snake_case")]
18pub enum MajorityType {
19 Absolute,
22 TwoThirds,
25 FourFifths,
28 Unanimity,
31}
32
33impl MajorityType {
34 fn rang(&self) -> u8 {
37 match self {
38 MajorityType::Absolute => 0,
39 MajorityType::TwoThirds => 1,
40 MajorityType::FourFifths => 2,
41 MajorityType::Unanimity => 3,
42 }
43 }
44
45 pub fn satisfait(&self, requise: &MajorityType) -> bool {
53 self.rang() >= requise.rang()
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
59#[serde(rename_all = "snake_case")]
60pub enum ResolutionStatus {
61 Pending, Adopted, Rejected, }
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
76#[serde(rename_all = "snake_case")]
77pub enum ResolutionKind {
78 #[default]
79 Standard,
80 EvaluationContractorsAuto,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
85pub struct Resolution {
86 pub id: Uuid,
87 pub meeting_id: Uuid,
88 pub title: String,
89 pub description: String,
90 pub resolution_type: ResolutionType,
91 pub majority_required: MajorityType,
92 pub kind: ResolutionKind,
94 pub vote_count_pour: i32,
95 pub vote_count_contre: i32,
96 pub vote_count_abstention: i32,
97 pub total_voting_power_pour: Decimal,
98 pub total_voting_power_contre: Decimal,
99 pub total_voting_power_abstention: Decimal,
100 pub status: ResolutionStatus,
101 pub agenda_item_index: Option<usize>, pub prestataire_de_la_mission: Option<Uuid>,
119 pub created_at: DateTime<Utc>,
120 pub voted_at: Option<DateTime<Utc>>,
121}
122
123fn pourcentage(part: Decimal, total: Decimal) -> f64 {
130 if total <= Decimal::ZERO {
131 return 0.0;
132 }
133 (part * Decimal::from(100) / total)
134 .to_string()
135 .parse()
136 .unwrap_or(0.0)
137}
138
139impl Resolution {
140 pub fn new(
143 meeting_id: Uuid,
144 title: String,
145 description: String,
146 resolution_type: ResolutionType,
147 majority_required: MajorityType,
148 agenda_item_index: Option<usize>,
149 ) -> Result<Self, String> {
150 Self::new_avec_prestataire(
151 meeting_id,
152 title,
153 description,
154 resolution_type,
155 majority_required,
156 agenda_item_index,
157 None,
158 )
159 }
160
161 #[allow(clippy::too_many_arguments)]
164 pub fn new_avec_prestataire(
165 meeting_id: Uuid,
166 title: String,
167 description: String,
168 resolution_type: ResolutionType,
169 majority_required: MajorityType,
170 agenda_item_index: Option<usize>,
171 prestataire_de_la_mission: Option<Uuid>,
172 ) -> Result<Self, String> {
173 if title.is_empty() {
174 return Err("Resolution title cannot be empty".to_string());
175 }
176 if description.is_empty() {
177 return Err("Resolution description cannot be empty".to_string());
178 }
179
180 let now = Utc::now();
181 Ok(Self {
182 id: Uuid::new_v4(),
183 meeting_id,
184 title,
185 description,
186 resolution_type,
187 majority_required,
188 vote_count_pour: 0,
189 vote_count_contre: 0,
190 vote_count_abstention: 0,
191 total_voting_power_pour: Decimal::ZERO,
192 total_voting_power_contre: Decimal::ZERO,
193 total_voting_power_abstention: Decimal::ZERO,
194 status: ResolutionStatus::Pending,
195 agenda_item_index,
196 prestataire_de_la_mission,
197 created_at: now,
198 voted_at: None,
199 kind: ResolutionKind::Standard,
200 })
201 }
202
203 pub fn new_evaluation_contractors_auto(meeting_id: Uuid) -> Self {
212 let now = Utc::now();
213 Self {
214 id: Uuid::new_v4(),
215 meeting_id,
216 title: "Évaluation des prestataires".to_string(),
217 description: "Évaluation de l'exécution des contrats en cours, inscrite d'office \
218 à l'ordre du jour de l'assemblée ordinaire (Art. 3.89 § 5, 12° Code Civil belge)."
219 .to_string(),
220 resolution_type: ResolutionType::Ordinary,
221 majority_required: MajorityType::Absolute,
222 vote_count_pour: 0,
223 vote_count_contre: 0,
224 vote_count_abstention: 0,
225 total_voting_power_pour: Decimal::ZERO,
226 total_voting_power_contre: Decimal::ZERO,
227 total_voting_power_abstention: Decimal::ZERO,
228 status: ResolutionStatus::Pending,
229 agenda_item_index: None,
230 prestataire_de_la_mission: None,
231 created_at: now,
232 voted_at: None,
233 kind: ResolutionKind::EvaluationContractorsAuto,
234 }
235 }
236
237 pub fn is_auto_generated(&self) -> bool {
244 self.kind == ResolutionKind::EvaluationContractorsAuto
245 }
246
247 pub fn record_vote_pour(&mut self, voting_power: Decimal) {
249 self.vote_count_pour += 1;
250 self.total_voting_power_pour += voting_power;
251 }
252
253 pub fn record_vote_contre(&mut self, voting_power: Decimal) {
255 self.vote_count_contre += 1;
256 self.total_voting_power_contre += voting_power;
257 }
258
259 pub fn record_abstention(&mut self, voting_power: Decimal) {
261 self.vote_count_abstention += 1;
262 self.total_voting_power_abstention += voting_power;
263 }
264
265 pub fn recompter_avec(&mut self, votes: &[super::vote::Vote], poids_retenus: &[Decimal]) {
276 use super::vote::VoteChoice;
277
278 self.total_voting_power_pour = Decimal::ZERO;
279 self.total_voting_power_contre = Decimal::ZERO;
280 self.total_voting_power_abstention = Decimal::ZERO;
281
282 for (vote, poids) in votes.iter().zip(poids_retenus) {
283 match vote.vote_choice {
284 VoteChoice::Pour => self.total_voting_power_pour += poids,
285 VoteChoice::Contre => self.total_voting_power_contre += poids,
286 VoteChoice::Abstention => self.total_voting_power_abstention += poids,
287 }
288 }
289 }
290
291 pub fn calculate_result(&self, total_voting_power: Decimal) -> ResolutionStatus {
293 let expressed = self.total_voting_power_pour + self.total_voting_power_contre;
294
295 match &self.majority_required {
296 MajorityType::Absolute => {
297 if expressed > Decimal::ZERO && self.total_voting_power_pour > expressed / dec!(2) {
299 ResolutionStatus::Adopted
300 } else {
301 ResolutionStatus::Rejected
302 }
303 }
304 MajorityType::TwoThirds => {
305 if expressed > Decimal::ZERO
307 && self.total_voting_power_pour / expressed >= dec!(2) / dec!(3)
308 {
309 ResolutionStatus::Adopted
310 } else {
311 ResolutionStatus::Rejected
312 }
313 }
314 MajorityType::FourFifths => {
315 if expressed > Decimal::ZERO
317 && self.total_voting_power_pour / expressed >= dec!(4) / dec!(5)
318 {
319 ResolutionStatus::Adopted
320 } else {
321 ResolutionStatus::Rejected
322 }
323 }
324 MajorityType::Unanimity => {
325 if total_voting_power > Decimal::ZERO
328 && (self.total_voting_power_pour - total_voting_power).abs() < dec!(0.01)
329 {
330 ResolutionStatus::Adopted
331 } else {
332 ResolutionStatus::Rejected
333 }
334 }
335 }
336 }
337
338 pub fn close_voting(&mut self, total_voting_power: Decimal) -> Result<(), String> {
340 if self.status != ResolutionStatus::Pending {
341 return Err("Voting already closed for this resolution".to_string());
342 }
343
344 self.status = self.calculate_result(total_voting_power);
345 self.voted_at = Some(Utc::now());
346 Ok(())
347 }
348
349 pub fn total_votes(&self) -> i32 {
351 self.vote_count_pour + self.vote_count_contre + self.vote_count_abstention
352 }
353
354 pub fn voix_exprimees(&self) -> Decimal {
361 self.total_voting_power_pour + self.total_voting_power_contre
362 }
363
364 pub fn pour_percentage(&self) -> f64 {
378 pourcentage(self.total_voting_power_pour, self.voix_exprimees())
379 }
380
381 pub fn contre_percentage(&self) -> f64 {
383 pourcentage(self.total_voting_power_contre, self.voix_exprimees())
384 }
385
386 pub fn abstention_percentage(&self) -> f64 {
394 let presentes = self.voix_exprimees() + self.total_voting_power_abstention;
395 pourcentage(self.total_voting_power_abstention, presentes)
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn test_create_resolution_success() {
405 let meeting_id = Uuid::new_v4();
406 let resolution = Resolution::new(
407 meeting_id,
408 "Approbation des comptes 2024".to_string(),
409 "Vote pour approuver les comptes annuels de l'exercice 2024".to_string(),
410 ResolutionType::Ordinary,
411 MajorityType::Absolute,
412 Some(0),
413 );
414
415 assert!(resolution.is_ok());
416 let resolution = resolution.unwrap();
417 assert_eq!(resolution.meeting_id, meeting_id);
418 assert_eq!(resolution.status, ResolutionStatus::Pending);
419 assert_eq!(resolution.total_votes(), 0);
420 assert_eq!(resolution.agenda_item_index, Some(0));
421 }
422
423 #[test]
424 fn test_create_resolution_without_agenda_item() {
425 let meeting_id = Uuid::new_v4();
426 let resolution = Resolution::new(
427 meeting_id,
428 "Approbation des comptes 2024".to_string(),
429 "Vote pour approuver les comptes annuels de l'exercice 2024".to_string(),
430 ResolutionType::Ordinary,
431 MajorityType::Absolute,
432 None,
433 );
434
435 assert!(resolution.is_ok());
436 let resolution = resolution.unwrap();
437 assert_eq!(resolution.agenda_item_index, None);
438 }
439
440 #[test]
441 fn test_create_resolution_empty_title_fails() {
442 let meeting_id = Uuid::new_v4();
443 let resolution = Resolution::new(
444 meeting_id,
445 "".to_string(),
446 "Description".to_string(),
447 ResolutionType::Ordinary,
448 MajorityType::Absolute,
449 Some(0),
450 );
451
452 assert!(resolution.is_err());
453 assert_eq!(resolution.unwrap_err(), "Resolution title cannot be empty");
454 }
455
456 #[test]
457 fn test_record_votes() {
458 let meeting_id = Uuid::new_v4();
459 let mut resolution = Resolution::new(
460 meeting_id,
461 "Test Resolution".to_string(),
462 "Description".to_string(),
463 ResolutionType::Ordinary,
464 MajorityType::Absolute,
465 Some(0),
466 )
467 .unwrap();
468
469 resolution.record_vote_pour(dec!(100));
470 resolution.record_vote_pour(dec!(150));
471 resolution.record_vote_contre(dec!(200));
472 resolution.record_abstention(dec!(50));
473
474 assert_eq!(resolution.vote_count_pour, 2);
475 assert_eq!(resolution.vote_count_contre, 1);
476 assert_eq!(resolution.vote_count_abstention, 1);
477 assert_eq!(resolution.total_voting_power_pour, dec!(250));
478 assert_eq!(resolution.total_voting_power_contre, dec!(200));
479 assert_eq!(resolution.total_voting_power_abstention, dec!(50));
480 assert_eq!(resolution.total_votes(), 4);
481 }
482
483 #[test]
486 fn test_calculate_result_absolute_majority_adopted() {
487 let meeting_id = Uuid::new_v4();
488 let mut resolution = Resolution::new(
489 meeting_id,
490 "Test Resolution".to_string(),
491 "Description".to_string(),
492 ResolutionType::Ordinary,
493 MajorityType::Absolute,
494 Some(0),
495 )
496 .unwrap();
497
498 resolution.record_vote_pour(dec!(300));
500 resolution.record_vote_contre(dec!(150));
501 resolution.record_abstention(dec!(50));
502
503 let result = resolution.calculate_result(dec!(1000));
504 assert_eq!(result, ResolutionStatus::Adopted);
505 }
506
507 #[test]
508 fn test_calculate_result_absolute_majority_rejected() {
509 let meeting_id = Uuid::new_v4();
510 let mut resolution = Resolution::new(
511 meeting_id,
512 "Test Resolution".to_string(),
513 "Description".to_string(),
514 ResolutionType::Ordinary,
515 MajorityType::Absolute,
516 Some(0),
517 )
518 .unwrap();
519
520 resolution.record_vote_pour(dec!(150));
522 resolution.record_vote_contre(dec!(300));
523 resolution.record_abstention(dec!(50));
524
525 let result = resolution.calculate_result(dec!(1000));
526 assert_eq!(result, ResolutionStatus::Rejected);
527 }
528
529 #[test]
530 fn test_absolute_majority_abstentions_excluded() {
531 let meeting_id = Uuid::new_v4();
532 let mut resolution = Resolution::new(
533 meeting_id,
534 "Test Resolution".to_string(),
535 "Description".to_string(),
536 ResolutionType::Ordinary,
537 MajorityType::Absolute,
538 Some(0),
539 )
540 .unwrap();
541
542 resolution.record_vote_pour(dec!(300));
545 resolution.record_vote_contre(dec!(200));
546 resolution.record_abstention(dec!(500));
547
548 let result = resolution.calculate_result(dec!(1000));
549 assert_eq!(result, ResolutionStatus::Adopted);
550 }
551
552 #[test]
555 fn test_calculate_result_two_thirds_majority_adopted() {
556 let meeting_id = Uuid::new_v4();
557 let mut resolution = Resolution::new(
558 meeting_id,
559 "Test Resolution".to_string(),
560 "Description".to_string(),
561 ResolutionType::Extraordinary,
562 MajorityType::TwoThirds,
563 Some(0),
564 )
565 .unwrap();
566
567 resolution.record_vote_pour(dec!(700));
569 resolution.record_vote_contre(dec!(200));
570 resolution.record_abstention(dec!(100));
571
572 let result = resolution.calculate_result(dec!(1000));
573 assert_eq!(result, ResolutionStatus::Adopted);
574 }
575
576 #[test]
577 fn test_calculate_result_two_thirds_majority_rejected() {
578 let meeting_id = Uuid::new_v4();
579 let mut resolution = Resolution::new(
580 meeting_id,
581 "Test Resolution".to_string(),
582 "Description".to_string(),
583 ResolutionType::Extraordinary,
584 MajorityType::TwoThirds,
585 Some(0),
586 )
587 .unwrap();
588
589 resolution.record_vote_pour(dec!(600));
592 resolution.record_vote_contre(dec!(300));
593 resolution.record_abstention(dec!(100));
594
595 let result = resolution.calculate_result(dec!(1000));
596 assert_eq!(result, ResolutionStatus::Adopted);
597 }
598
599 #[test]
600 fn test_two_thirds_majority_barely_rejected() {
601 let meeting_id = Uuid::new_v4();
602 let mut resolution = Resolution::new(
603 meeting_id,
604 "Test Resolution".to_string(),
605 "Description".to_string(),
606 ResolutionType::Extraordinary,
607 MajorityType::TwoThirds,
608 Some(0),
609 )
610 .unwrap();
611
612 resolution.record_vote_pour(dec!(500));
614 resolution.record_vote_contre(dec!(300));
615 resolution.record_abstention(dec!(200));
616
617 let result = resolution.calculate_result(dec!(1000));
618 assert_eq!(result, ResolutionStatus::Rejected);
619 }
620
621 #[test]
622 fn test_two_thirds_abstentions_excluded() {
623 let meeting_id = Uuid::new_v4();
624 let mut resolution = Resolution::new(
625 meeting_id,
626 "Test Resolution".to_string(),
627 "Description".to_string(),
628 ResolutionType::Extraordinary,
629 MajorityType::TwoThirds,
630 Some(0),
631 )
632 .unwrap();
633
634 resolution.record_vote_pour(dec!(400));
636 resolution.record_vote_contre(dec!(100));
637 resolution.record_abstention(dec!(500));
638
639 let result = resolution.calculate_result(dec!(1000));
640 assert_eq!(result, ResolutionStatus::Adopted);
641 }
642
643 #[test]
646 fn test_calculate_result_four_fifths_majority_adopted() {
647 let meeting_id = Uuid::new_v4();
648 let mut resolution = Resolution::new(
649 meeting_id,
650 "Test Resolution".to_string(),
651 "Description".to_string(),
652 ResolutionType::Extraordinary,
653 MajorityType::FourFifths,
654 Some(0),
655 )
656 .unwrap();
657
658 resolution.record_vote_pour(dec!(800));
660 resolution.record_vote_contre(dec!(100));
661 resolution.record_abstention(dec!(100));
662
663 let result = resolution.calculate_result(dec!(1000));
664 assert_eq!(result, ResolutionStatus::Adopted);
665 }
666
667 #[test]
668 fn test_calculate_result_four_fifths_majority_rejected() {
669 let meeting_id = Uuid::new_v4();
670 let mut resolution = Resolution::new(
671 meeting_id,
672 "Test Resolution".to_string(),
673 "Description".to_string(),
674 ResolutionType::Extraordinary,
675 MajorityType::FourFifths,
676 Some(0),
677 )
678 .unwrap();
679
680 resolution.record_vote_pour(dec!(700));
682 resolution.record_vote_contre(dec!(200));
683 resolution.record_abstention(dec!(100));
684
685 let result = resolution.calculate_result(dec!(1000));
686 assert_eq!(result, ResolutionStatus::Rejected);
687 }
688
689 #[test]
690 fn test_four_fifths_abstentions_excluded() {
691 let meeting_id = Uuid::new_v4();
692 let mut resolution = Resolution::new(
693 meeting_id,
694 "Test Resolution".to_string(),
695 "Description".to_string(),
696 ResolutionType::Extraordinary,
697 MajorityType::FourFifths,
698 Some(0),
699 )
700 .unwrap();
701
702 resolution.record_vote_pour(dec!(400));
704 resolution.record_vote_contre(dec!(50));
705 resolution.record_abstention(dec!(550));
706
707 let result = resolution.calculate_result(dec!(1000));
708 assert_eq!(result, ResolutionStatus::Adopted);
709 }
710
711 #[test]
714 fn test_calculate_result_unanimity_adopted() {
715 let meeting_id = Uuid::new_v4();
716 let mut resolution = Resolution::new(
717 meeting_id,
718 "Test Resolution".to_string(),
719 "Description".to_string(),
720 ResolutionType::Extraordinary,
721 MajorityType::Unanimity,
722 Some(0),
723 )
724 .unwrap();
725
726 resolution.record_vote_pour(dec!(10000));
728
729 let result = resolution.calculate_result(dec!(10000));
730 assert_eq!(result, ResolutionStatus::Adopted);
731 }
732
733 #[test]
734 fn test_calculate_result_unanimity_rejected_missing_votes() {
735 let meeting_id = Uuid::new_v4();
736 let mut resolution = Resolution::new(
737 meeting_id,
738 "Test Resolution".to_string(),
739 "Description".to_string(),
740 ResolutionType::Extraordinary,
741 MajorityType::Unanimity,
742 Some(0),
743 )
744 .unwrap();
745
746 resolution.record_vote_pour(dec!(9000));
748
749 let result = resolution.calculate_result(dec!(10000));
750 assert_eq!(result, ResolutionStatus::Rejected);
751 }
752
753 #[test]
754 fn test_unanimity_requires_all_tantiemes_not_just_present() {
755 let meeting_id = Uuid::new_v4();
756 let mut resolution = Resolution::new(
757 meeting_id,
758 "Test Resolution".to_string(),
759 "Description".to_string(),
760 ResolutionType::Extraordinary,
761 MajorityType::Unanimity,
762 Some(0),
763 )
764 .unwrap();
765
766 resolution.record_vote_pour(dec!(8000));
769
770 let result = resolution.calculate_result(dec!(10000));
771 assert_eq!(result, ResolutionStatus::Rejected);
772 }
773
774 #[test]
775 fn test_unanimity_rejected_with_abstention() {
776 let meeting_id = Uuid::new_v4();
777 let mut resolution = Resolution::new(
778 meeting_id,
779 "Test Resolution".to_string(),
780 "Description".to_string(),
781 ResolutionType::Extraordinary,
782 MajorityType::Unanimity,
783 Some(0),
784 )
785 .unwrap();
786
787 resolution.record_vote_pour(dec!(9500));
789 resolution.record_abstention(dec!(500));
790
791 let result = resolution.calculate_result(dec!(10000));
792 assert_eq!(result, ResolutionStatus::Rejected);
793 }
794
795 #[test]
798 fn test_close_voting_success() {
799 let meeting_id = Uuid::new_v4();
800 let mut resolution = Resolution::new(
801 meeting_id,
802 "Test Resolution".to_string(),
803 "Description".to_string(),
804 ResolutionType::Ordinary,
805 MajorityType::Absolute,
806 Some(0),
807 )
808 .unwrap();
809
810 resolution.record_vote_pour(dec!(300));
811 resolution.record_vote_contre(dec!(150));
812
813 let result = resolution.close_voting(dec!(1000));
814 assert!(result.is_ok());
815 assert_eq!(resolution.status, ResolutionStatus::Adopted);
816 assert!(resolution.voted_at.is_some());
817 }
818
819 #[test]
820 fn test_close_voting_already_closed_fails() {
821 let meeting_id = Uuid::new_v4();
822 let mut resolution = Resolution::new(
823 meeting_id,
824 "Test Resolution".to_string(),
825 "Description".to_string(),
826 ResolutionType::Ordinary,
827 MajorityType::Absolute,
828 Some(0),
829 )
830 .unwrap();
831
832 resolution.record_vote_pour(dec!(300));
833 resolution.close_voting(dec!(1000)).unwrap();
834
835 let result = resolution.close_voting(dec!(1000));
836 assert!(result.is_err());
837 assert_eq!(
838 result.unwrap_err(),
839 "Voting already closed for this resolution"
840 );
841 }
842
843 #[test]
844 fn test_percentages() {
845 let meeting_id = Uuid::new_v4();
846 let mut resolution = Resolution::new(
847 meeting_id,
848 "Test Resolution".to_string(),
849 "Description".to_string(),
850 ResolutionType::Ordinary,
851 MajorityType::Absolute,
852 Some(0),
853 )
854 .unwrap();
855
856 resolution.record_vote_pour(dec!(100));
857 resolution.record_vote_pour(dec!(100)); resolution.record_vote_contre(dec!(100)); resolution.record_abstention(dec!(100)); assert!((resolution.pour_percentage() - 66.666_666).abs() < 0.001);
863 assert!((resolution.contre_percentage() - 33.333_333).abs() < 0.001);
864 assert_eq!(resolution.abstention_percentage(), 25.0);
866 }
867
868 #[test]
875 fn les_pourcentages_comptent_des_voix_pas_des_tetes() {
876 let mut resolution = Resolution::new(
877 Uuid::new_v4(),
878 "Approbation des comptes".to_string(),
879 "Comptes annuels".to_string(),
880 ResolutionType::Ordinary,
881 MajorityType::Absolute,
882 Some(1),
883 )
884 .unwrap();
885
886 resolution.record_vote_pour(dec!(550)); resolution.record_vote_contre(dec!(250));
888 resolution.record_vote_contre(dec!(200));
889
890 assert_eq!(resolution.vote_count_pour, 1);
892 assert_eq!(resolution.vote_count_contre, 2);
893 assert_eq!(resolution.pour_percentage(), 55.0);
894 assert_eq!(resolution.contre_percentage(), 45.0);
895 }
896
897 #[test]
901 fn happy_deux_tiers_satisfait_deux_tiers() {
902 assert!(MajorityType::TwoThirds.satisfait(&MajorityType::TwoThirds));
903 }
904
905 #[test]
908 fn edge_unanimite_satisfait_deux_tiers() {
909 assert!(MajorityType::Unanimity.satisfait(&MajorityType::TwoThirds));
910 assert!(MajorityType::FourFifths.satisfait(&MajorityType::TwoThirds));
911 }
912
913 #[test]
917 fn security_majorite_absolue_ne_satisfait_pas_deux_tiers() {
918 assert!(!MajorityType::Absolute.satisfait(&MajorityType::TwoThirds));
919 }
920
921 #[test]
924 fn negative_quatre_cinquiemes_ne_satisfait_pas_unanimite() {
925 assert!(!MajorityType::FourFifths.satisfait(&MajorityType::Unanimity));
926 }
927
928 #[test]
933 fn happy_resolution_evaluation_contractors_auto_est_auto_generee() {
934 let resolution = Resolution::new_evaluation_contractors_auto(Uuid::new_v4());
935 assert_eq!(resolution.kind, ResolutionKind::EvaluationContractorsAuto);
936 assert!(resolution.is_auto_generated());
937 assert_eq!(resolution.status, ResolutionStatus::Pending);
938 }
939
940 #[test]
943 fn edge_resolution_standard_homonyme_nest_pas_auto_generee() {
944 let resolution = Resolution::new(
945 Uuid::new_v4(),
946 "Évaluation des prestataires".to_string(),
947 "Point ajouté manuellement par le syndic".to_string(),
948 ResolutionType::Ordinary,
949 MajorityType::Absolute,
950 None,
951 )
952 .unwrap();
953 assert!(!resolution.is_auto_generated());
954 }
955
956 #[test]
960 fn security_is_auto_generated_repose_uniquement_sur_kind() {
961 let mut resolution = Resolution::new_evaluation_contractors_auto(Uuid::new_v4());
962 resolution.status = ResolutionStatus::Adopted;
963 resolution.title = "Renommée par un tiers".to_string();
964 assert!(resolution.is_auto_generated());
965 }
966
967 #[test]
971 fn negative_deux_resolutions_auto_generees_ne_partagent_rien() {
972 let meeting_a = Uuid::new_v4();
973 let meeting_b = Uuid::new_v4();
974 let a = Resolution::new_evaluation_contractors_auto(meeting_a);
975 let b = Resolution::new_evaluation_contractors_auto(meeting_b);
976 assert_ne!(a.id, b.id);
977 assert_eq!(a.meeting_id, meeting_a);
978 assert_eq!(b.meeting_id, meeting_b);
979 }
980
981 #[test]
983 fn sans_vote_les_pourcentages_valent_zero() {
984 let resolution = Resolution::new(
985 Uuid::new_v4(),
986 "Rien".to_string(),
987 "Aucun vote".to_string(),
988 ResolutionType::Ordinary,
989 MajorityType::Absolute,
990 Some(1),
991 )
992 .unwrap();
993
994 assert_eq!(resolution.pour_percentage(), 0.0);
995 assert_eq!(resolution.contre_percentage(), 0.0);
996 assert_eq!(resolution.abstention_percentage(), 0.0);
997 }
998}