1use crate::application::error::AppError;
2use crate::application::ports::{
3 MeetingRepository, ResolutionRepository, UnitOwnerRepository, UnitRepository, VoteRepository,
4};
5use crate::domain::entities::{
6 assert_vote_auth_sufficient, assert_voting_right_active, MajorityType, Resolution,
7 ResolutionStatus, ResolutionType, Vote, VoteAuthMethod, VoteChoice,
8};
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub struct ResolutionUseCases {
13 resolution_repository: Arc<dyn ResolutionRepository>,
14 vote_repository: Arc<dyn VoteRepository>,
15 meeting_repository: Arc<dyn MeetingRepository>,
16 unit_owner_repository: Arc<dyn UnitOwnerRepository>,
18 unit_repository: Arc<dyn UnitRepository>,
20}
21
22impl ResolutionUseCases {
23 pub fn new(
24 resolution_repository: Arc<dyn ResolutionRepository>,
25 vote_repository: Arc<dyn VoteRepository>,
26 meeting_repository: Arc<dyn MeetingRepository>,
27 unit_owner_repository: Arc<dyn UnitOwnerRepository>,
28 unit_repository: Arc<dyn UnitRepository>,
29 ) -> Self {
30 Self {
31 resolution_repository,
32 vote_repository,
33 meeting_repository,
34 unit_owner_repository,
35 unit_repository,
36 }
37 }
38
39 pub async fn create_resolution(
43 &self,
44 meeting_id: Uuid,
45 title: String,
46 description: String,
47 resolution_type: ResolutionType,
48 majority_required: MajorityType,
49 agenda_item_index: Option<usize>,
50 ) -> Result<Resolution, String> {
51 let meeting = self
53 .meeting_repository
54 .find_by_id(meeting_id)
55 .await?
56 .ok_or_else(|| format!("Meeting not found: {}", meeting_id))?;
57
58 meeting.check_quorum_for_voting()?;
60
61 if let Some(index) = agenda_item_index {
63 if index >= meeting.agenda.len() {
64 return Err(
65 "Resolution must correspond to a valid agenda item (Art. 3.87 CC)".to_string(),
66 );
67 }
68 let agenda_item = &meeting.agenda[index];
69 if agenda_item.trim().is_empty() {
70 return Err("Agenda item cannot be empty (Art. 3.87 CC)".to_string());
71 }
72 }
73
74 let resolution = Resolution::new(
75 meeting_id,
76 title,
77 description,
78 resolution_type,
79 majority_required,
80 agenda_item_index,
81 )?;
82
83 self.resolution_repository.create(&resolution).await
84 }
85
86 pub async fn get_resolution(&self, id: Uuid) -> Result<Option<Resolution>, String> {
88 let resolution = self.resolution_repository.find_by_id(id).await?;
89 match resolution {
90 Some(r) => Ok(Some(self.plafonner_pour_lecture(r).await)),
91 None => Ok(None),
92 }
93 }
94
95 async fn plafonner_pour_lecture(&self, mut resolution: Resolution) -> Resolution {
122 let Ok(votes) = self
123 .vote_repository
124 .find_by_resolution_id(resolution.id)
125 .await
126 else {
127 return resolution;
128 };
129 if votes.is_empty() {
130 return resolution;
131 }
132 let decompte = crate::domain::copropriete::plafonner_les_voix(&votes);
133 if decompte.ecarts().is_empty() {
134 return resolution;
135 }
136 let poids_retenus = crate::domain::copropriete::repartir_le_plafond(&votes, &decompte);
137 resolution.recompter_avec(&votes, &poids_retenus);
138 resolution
139 }
140
141 pub async fn get_meeting_resolutions(
143 &self,
144 meeting_id: Uuid,
145 ) -> Result<Vec<Resolution>, String> {
146 let resolutions = self
147 .resolution_repository
148 .find_by_meeting_id(meeting_id)
149 .await?;
150 let mut plafonnees = Vec::with_capacity(resolutions.len());
153 for r in resolutions {
154 plafonnees.push(self.plafonner_pour_lecture(r).await);
155 }
156 Ok(plafonnees)
157 }
158
159 pub async fn get_resolutions_by_status(
161 &self,
162 status: ResolutionStatus,
163 ) -> Result<Vec<Resolution>, String> {
164 self.resolution_repository.find_by_status(status).await
165 }
166
167 pub async fn update_resolution(&self, resolution: &Resolution) -> Result<Resolution, AppError> {
173 if resolution.is_auto_generated() {
174 return Err(AppError::ResolutionAutoNotRemovable);
175 }
176 if resolution.status != ResolutionStatus::Pending {
177 return Err(AppError::Validation(
178 "Cannot update a resolution that is not pending".to_string(),
179 ));
180 }
181
182 self.resolution_repository
183 .update(resolution)
184 .await
185 .map_err(AppError::from)
186 }
187
188 pub async fn delete_resolution(&self, id: Uuid) -> Result<bool, AppError> {
194 let resolution = self
195 .resolution_repository
196 .find_by_id(id)
197 .await
198 .map_err(AppError::from)?
199 .ok_or_else(|| AppError::NotFound(format!("resolution {}", id)))?;
200
201 if resolution.is_auto_generated() {
202 return Err(AppError::ResolutionAutoNotRemovable);
203 }
204
205 let votes = self
207 .vote_repository
208 .find_by_resolution_id(id)
209 .await
210 .map_err(AppError::from)?;
211 if !votes.is_empty() {
212 return Err(AppError::Validation(
213 "Cannot delete a resolution with existing votes".to_string(),
214 ));
215 }
216
217 self.resolution_repository
218 .delete(id)
219 .await
220 .map_err(AppError::from)
221 }
222
223 #[allow(clippy::too_many_arguments)]
236 pub async fn cast_vote(
237 &self,
238 resolution_id: Uuid,
239 owner_id: Uuid,
240 unit_id: Uuid,
241 vote_choice: VoteChoice,
242 proxy_owner_id: Option<Uuid>,
243 auth_method: Option<VoteAuthMethod>,
244 caller_owner_id: Uuid,
245 ) -> Result<Vote, String> {
246 let resolution = self
248 .resolution_repository
249 .find_by_id(resolution_id)
250 .await?
251 .ok_or_else(|| "Resolution not found".to_string())?;
252
253 if resolution.status != ResolutionStatus::Pending {
254 return Err("Cannot vote on a resolution that is not pending".to_string());
255 }
256
257 let meeting = self
262 .meeting_repository
263 .find_by_id(resolution.meeting_id)
264 .await?
265 .ok_or_else(|| format!("Meeting not found: {}", resolution.meeting_id))?;
266 meeting.check_quorum_for_voting()?;
267
268 let Some(index) = resolution.agenda_item_index else {
295 return Err(
296 "Cette résolution n'est rattachée à aucun point de l'ordre du jour : \
301 la mettre aux voix produirait une décision nulle (Art. 3.87 § 2 CC). \
302 Inscrivez le point à l'ordre du jour, puis recréez la résolution \
303 en la rattachant à ce point."
304 .to_string(),
305 );
306 };
307 match meeting.agenda.get(index) {
310 None => {
311 return Err(format!(
312 "Le point d'ordre du jour n° {index} n'existe pas dans cette \
313 assemblée, qui en compte {} : la décision serait nulle \
314 (Art. 3.87 § 2 CC).",
315 meeting.agenda.len()
316 ))
317 }
318 Some(intitule) if intitule.trim().is_empty() => {
319 return Err(format!(
320 "Le point d'ordre du jour n° {index} est vide : une décision ne \
321 peut pas porter sur un point qui n'énonce rien \
322 (Art. 3.87 § 2 CC)."
323 ))
324 }
325 Some(_) => {}
326 }
327
328 let holders = self
335 .unit_owner_repository
336 .find_voting_holders_by_unit(unit_id)
337 .await?;
338 assert_voting_right_active(unit_id, &holders)?;
339
340 match proxy_owner_id {
363 Some(declare) if declare != caller_owner_id => {
364 return Err(format!(
365 "FORBIDDEN: {caller_owner_id} ne peut se déclarer mandataire \
366 que de lui-même, pas de {declare}."
367 ));
368 }
369 None if owner_id != caller_owner_id => {
370 return Err(format!(
371 "FORBIDDEN: {caller_owner_id} ne peut pas voter au nom du \
372 copropriétaire {owner_id} sans se déclarer son mandataire \
373 (Art. 3.87 § 7 CC)."
374 ));
375 }
376 Some(_) | None => {}
377 }
378
379 if self
384 .unit_owner_repository
385 .find_active_by_unit_and_owner(unit_id, owner_id)
386 .await?
387 .is_none()
388 {
389 return Err(format!(
390 "FORBIDDEN: le copropriétaire {owner_id} ne détient pas le lot \
391 {unit_id} : il ne peut pas voter pour lui (Art. 3.87 § 1er CC)."
392 ));
393 }
394
395 if self
397 .vote_repository
398 .has_voted(resolution_id, unit_id)
399 .await?
400 {
401 return Err("This unit has already voted on this resolution".to_string());
402 }
403
404 let unit = self
421 .unit_repository
422 .find_by_id(unit_id)
423 .await?
424 .ok_or_else(|| format!("Lot introuvable : {unit_id}"))?;
425
426 if unit.building_id != meeting.building_id {
431 return Err(format!(
432 "Le lot {unit_id} n'appartient pas à l'immeuble de cette \
433 assemblée : il ne peut pas prendre part à ce vote."
434 ));
435 }
436
437 let voting_power = unit.quota;
438
439 if let Some(proxy_id) = proxy_owner_id {
442 self.validate_proxy_limit(resolution_id, proxy_id, voting_power)
443 .await?;
444 }
445
446 let auth_method =
453 assert_vote_auth_sufficient(meeting.mode, auth_method, proxy_owner_id.is_some())?;
454
455 let vote = Vote::new_with_auth_method(
457 resolution_id,
458 owner_id,
459 unit_id,
460 vote_choice.clone(),
461 voting_power,
462 proxy_owner_id,
463 auth_method,
464 )?;
465
466 let created_vote = self.vote_repository.create(&vote).await?;
467
468 self.update_resolution_vote_counts(resolution_id).await?;
470
471 Ok(created_vote)
472 }
473
474 pub async fn change_vote(&self, vote_id: Uuid, new_choice: VoteChoice) -> Result<Vote, String> {
476 let mut vote = self
477 .vote_repository
478 .find_by_id(vote_id)
479 .await?
480 .ok_or_else(|| "Vote not found".to_string())?;
481
482 let resolution = self
484 .resolution_repository
485 .find_by_id(vote.resolution_id)
486 .await?
487 .ok_or_else(|| "Resolution not found".to_string())?;
488
489 if resolution.status != ResolutionStatus::Pending {
490 return Err("Cannot change vote on a closed resolution".to_string());
491 }
492
493 vote.change_vote(new_choice)?;
495 let updated_vote = self.vote_repository.update(&vote).await?;
496
497 self.update_resolution_vote_counts(vote.resolution_id)
499 .await?;
500
501 Ok(updated_vote)
502 }
503
504 pub async fn get_resolution_votes(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
506 self.vote_repository
507 .find_by_resolution_id(resolution_id)
508 .await
509 }
510
511 pub async fn get_owner_votes(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
513 self.vote_repository.find_by_owner_id(owner_id).await
514 }
515
516 pub async fn close_voting(
518 &self,
519 resolution_id: Uuid,
520 total_voting_power: rust_decimal::Decimal,
521 ) -> Result<Resolution, String> {
522 let mut resolution = self
523 .resolution_repository
524 .find_by_id(resolution_id)
525 .await?
526 .ok_or_else(|| "Resolution not found".to_string())?;
527
528 if resolution.status != ResolutionStatus::Pending {
529 return Err("Resolution voting is already closed".to_string());
530 }
531
532 let votes = self
542 .vote_repository
543 .find_by_resolution_id(resolution_id)
544 .await?;
545 crate::domain::copropriete::verifier_procurations(&votes, total_voting_power, None)
546 .map_err(|refus| refus.to_string())?;
547
548 crate::domain::copropriete::verifier_conflit_dinterets(
552 &votes,
553 resolution.prestataire_de_la_mission,
554 )
555 .map_err(|conflit| conflit.to_string())?;
556
557 let decompte = crate::domain::copropriete::plafonner_les_voix(&votes);
566 let voix_plafonnees = if decompte.ecarts().is_empty() {
567 None
568 } else {
569 let poids_retenus = crate::domain::copropriete::repartir_le_plafond(&votes, &decompte);
570 resolution.recompter_avec(&votes, &poids_retenus);
571 self.resolution_repository
577 .update_vote_counts(
578 resolution_id,
579 resolution.vote_count_pour,
580 resolution.vote_count_contre,
581 resolution.vote_count_abstention,
582 resolution.total_voting_power_pour,
583 resolution.total_voting_power_contre,
584 resolution.total_voting_power_abstention,
585 )
586 .await?;
587 Some(serde_json::json!(decompte
588 .ecarts()
589 .iter()
590 .map(|e| serde_json::json!({
591 "votant": e.votant,
592 "voix_brutes": e.voix_brutes,
593 "voix_retenues": e.voix_retenues,
594 }))
595 .collect::<Vec<_>>()))
596 };
597
598 resolution.close_voting(total_voting_power)?;
600
601 self.resolution_repository
603 .close_voting(resolution_id, resolution.status.clone(), voix_plafonnees)
604 .await?;
605
606 self.resolution_repository
608 .find_by_id(resolution_id)
609 .await?
610 .ok_or_else(|| "Resolution not found after closing".to_string())
611 }
612
613 pub async fn get_meeting_vote_summary(
615 &self,
616 meeting_id: Uuid,
617 ) -> Result<Vec<Resolution>, String> {
618 self.resolution_repository
619 .get_meeting_vote_summary(meeting_id)
620 .await
621 }
622
623 async fn update_resolution_vote_counts(&self, resolution_id: Uuid) -> Result<(), String> {
625 let (pour_count, contre_count, abstention_count) = self
627 .vote_repository
628 .count_by_resolution_and_choice(resolution_id)
629 .await?;
630
631 let (pour_power, contre_power, abstention_power) = self
633 .vote_repository
634 .sum_voting_power_by_resolution(resolution_id)
635 .await?;
636
637 self.resolution_repository
639 .update_vote_counts(
640 resolution_id,
641 pour_count,
642 contre_count,
643 abstention_count,
644 pour_power,
645 contre_power,
646 abstention_power,
647 )
648 .await
649 }
650
651 pub async fn has_unit_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
653 self.vote_repository.has_voted(resolution_id, unit_id).await
654 }
655
656 async fn validate_proxy_limit(
665 &self,
666 resolution_id: Uuid,
667 proxy_owner_id: Uuid,
668 new_voting_power: rust_decimal::Decimal,
669 ) -> Result<(), String> {
670 let (existing_count, existing_power) = self
671 .vote_repository
672 .count_proxy_votes_for_mandataire(resolution_id, proxy_owner_id)
673 .await?;
674
675 let total_proxy_power = existing_power + new_voting_power;
677
678 let resolution = self
681 .resolution_repository
682 .find_by_id(resolution_id)
683 .await?
684 .ok_or_else(|| "Resolution not found".to_string())?;
685
686 let total_all_votes = resolution.total_voting_power_pour
690 + resolution.total_voting_power_contre
691 + resolution.total_voting_power_abstention
692 + new_voting_power; if total_all_votes > rust_decimal::Decimal::ZERO
696 && (total_proxy_power / total_all_votes) < rust_decimal_macros::dec!(0.10)
697 {
698 return Ok(()); }
700
701 if existing_count >= 3 {
703 return Err(format!(
704 "Le mandataire détient déjà {} procurations. Maximum autorisé : 3 (Art. 3.87 §7 CC). \
705 Exception 10% non applicable (procurations représentent >{:.1}% des votes).",
706 existing_count,
707 if total_all_votes > rust_decimal::Decimal::ZERO {
708 (total_proxy_power / total_all_votes) * rust_decimal_macros::dec!(100)
709 } else {
710 rust_decimal::Decimal::ZERO
711 }
712 ));
713 }
714
715 Ok(())
716 }
717
718 pub async fn get_vote_statistics(&self, resolution_id: Uuid) -> Result<VoteStatistics, String> {
720 let resolution = self
721 .resolution_repository
722 .find_by_id(resolution_id)
723 .await?
724 .ok_or_else(|| "Resolution not found".to_string())?;
725
726 Ok(VoteStatistics {
727 total_votes: resolution.total_votes(),
728 vote_count_pour: resolution.vote_count_pour,
729 vote_count_contre: resolution.vote_count_contre,
730 vote_count_abstention: resolution.vote_count_abstention,
731 total_voting_power_pour: resolution.total_voting_power_pour,
732 total_voting_power_contre: resolution.total_voting_power_contre,
733 total_voting_power_abstention: resolution.total_voting_power_abstention,
734 pour_percentage: resolution.pour_percentage(),
735 contre_percentage: resolution.contre_percentage(),
736 abstention_percentage: resolution.abstention_percentage(),
737 status: resolution.status,
738 })
739 }
740}
741
742#[derive(Debug, Clone)]
744pub struct VoteStatistics {
745 pub total_votes: i32,
746 pub vote_count_pour: i32,
747 pub vote_count_contre: i32,
748 pub vote_count_abstention: i32,
749 pub total_voting_power_pour: rust_decimal::Decimal,
750 pub total_voting_power_contre: rust_decimal::Decimal,
751 pub total_voting_power_abstention: rust_decimal::Decimal,
752 pub pour_percentage: f64,
753 pub contre_percentage: f64,
754 pub abstention_percentage: f64,
755 pub status: ResolutionStatus,
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
764 use crate::application::dto::{PageRequest, UnitFilters};
765 use crate::application::ports::{
766 MeetingRepository, ResolutionRepository, UnitOwnerRepository, UnitRepository,
767 VoteRepository,
768 };
769 use crate::domain::entities::{
770 LotHolder, Meeting, MeetingMode, MeetingType, OwnershipType, Unit, UnitOwner, UnitType,
771 VoteChoice,
772 };
773 use actix_web::ResponseError;
774 use async_trait::async_trait;
775 use chrono::Utc;
776 use rust_decimal_macros::dec;
777 use std::collections::HashMap;
778 use std::sync::Mutex;
779
780 struct MockUnitOwnerRepository {
784 holders: Mutex<HashMap<Uuid, Vec<LotHolder>>>,
785 detentions_refusees: Mutex<std::collections::HashSet<(Uuid, Uuid)>>,
792 }
793
794 impl MockUnitOwnerRepository {
795 fn new() -> Self {
796 Self {
797 holders: Mutex::new(HashMap::new()),
798 detentions_refusees: Mutex::new(std::collections::HashSet::new()),
799 }
800 }
801
802 fn with_holders(unit_id: Uuid, holders: Vec<LotHolder>) -> Self {
803 let m = Self::new();
804 m.holders.lock().unwrap().insert(unit_id, holders);
805 m
806 }
807
808 fn refuse_detention(&self, unit_id: Uuid, owner_id: Uuid) {
811 self.detentions_refusees
812 .lock()
813 .unwrap()
814 .insert((unit_id, owner_id));
815 }
816 }
817
818 #[async_trait]
819 impl UnitOwnerRepository for MockUnitOwnerRepository {
820 async fn create(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
821 Ok(unit_owner.clone())
822 }
823 async fn find_by_id(&self, _id: Uuid) -> Result<Option<UnitOwner>, String> {
824 Ok(None)
825 }
826 async fn find_current_owners_by_unit(
827 &self,
828 _unit_id: Uuid,
829 ) -> Result<Vec<UnitOwner>, String> {
830 Ok(vec![])
831 }
832 async fn find_current_units_by_owner(
833 &self,
834 _owner_id: Uuid,
835 ) -> Result<Vec<UnitOwner>, String> {
836 Ok(vec![])
837 }
838 async fn find_all_owners_by_unit(&self, _unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
839 Ok(vec![])
840 }
841 async fn find_all_units_by_owner(&self, _owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
842 Ok(vec![])
843 }
844 async fn update(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
845 Ok(unit_owner.clone())
846 }
847 async fn delete(&self, _id: Uuid) -> Result<(), String> {
848 Ok(())
849 }
850 async fn has_active_owners(&self, _unit_id: Uuid) -> Result<bool, String> {
851 Ok(true)
852 }
853 async fn get_total_ownership_percentage(
854 &self,
855 _unit_id: Uuid,
856 ) -> Result<rust_decimal::Decimal, String> {
857 Ok(rust_decimal::Decimal::ONE)
858 }
859 async fn find_active_by_unit_and_owner(
860 &self,
861 unit_id: Uuid,
862 owner_id: Uuid,
863 ) -> Result<Option<UnitOwner>, String> {
864 if self
865 .detentions_refusees
866 .lock()
867 .unwrap()
868 .contains(&(unit_id, owner_id))
869 {
870 return Ok(None);
871 }
872 Ok(Some(
873 UnitOwner::new(unit_id, owner_id, rust_decimal::Decimal::ONE, true)
874 .expect("détention de test"),
875 ))
876 }
877 async fn find_active_by_building(
878 &self,
879 _building_id: Uuid,
880 ) -> Result<Vec<(Uuid, Uuid, rust_decimal::Decimal)>, String> {
881 Ok(vec![])
882 }
883
884 async fn find_active_quota_shares_by_building(
887 &self,
888 _building_id: Uuid,
889 ) -> Result<Vec<(Uuid, Uuid, rust_decimal::Decimal)>, String> {
890 Ok(vec![])
891 }
892 async fn find_voting_holders_by_unit(
893 &self,
894 unit_id: Uuid,
895 ) -> Result<Vec<LotHolder>, String> {
896 Ok(self
897 .holders
898 .lock()
899 .unwrap()
900 .get(&unit_id)
901 .cloned()
902 .unwrap_or_default())
903 }
904
905 async fn is_voting_representative(&self, _unit_owner_id: Uuid) -> Result<bool, String> {
906 Ok(false)
907 }
908
909 async fn set_voting_representative(&self, _unit_owner_id: Uuid) -> Result<(), String> {
910 Ok(())
911 }
912 }
913
914 struct MockUnitRepository {
925 units: Mutex<HashMap<Uuid, Unit>>,
926 defaut: Mutex<Option<(Uuid, rust_decimal::Decimal)>>,
927 }
928
929 impl MockUnitRepository {
930 fn new() -> Self {
931 Self {
932 units: Mutex::new(HashMap::new()),
933 defaut: Mutex::new(None),
934 }
935 }
936
937 fn enregistre(&self, unit: Unit) {
938 self.units.lock().unwrap().insert(unit.id, unit);
939 }
940
941 fn pose_defaut(&self, building_id: Uuid, quota: rust_decimal::Decimal) {
946 *self.defaut.lock().unwrap() = Some((building_id, quota));
947 }
948
949 fn lot(&self, unit_id: Uuid, building_id: Uuid, quota: rust_decimal::Decimal) {
951 let mut unit = Unit::new(
952 Uuid::new_v4(),
953 building_id,
954 format!("L{}", &unit_id.to_string()[..4]),
955 UnitType::Apartment,
956 Some(0),
957 50.0,
958 quota,
959 )
960 .expect("lot de test");
961 unit.id = unit_id;
962 self.enregistre(unit);
963 }
964 }
965
966 #[async_trait]
967 impl UnitRepository for MockUnitRepository {
968 async fn create(&self, unit: &Unit) -> Result<Unit, String> {
969 self.units.lock().unwrap().insert(unit.id, unit.clone());
970 Ok(unit.clone())
971 }
972
973 async fn find_by_id(&self, id: Uuid) -> Result<Option<Unit>, String> {
974 if let Some(unit) = self.units.lock().unwrap().get(&id).cloned() {
975 return Ok(Some(unit));
976 }
977 let defaut = *self.defaut.lock().unwrap();
978 Ok(defaut.map(|(building_id, quota)| {
979 let mut unit = Unit::new(
980 Uuid::new_v4(),
981 building_id,
982 format!("L{}", &id.to_string()[..4]),
983 UnitType::Apartment,
984 Some(0),
985 50.0,
986 quota,
987 )
988 .expect("lot de test");
989 unit.id = id;
990 unit
991 }))
992 }
993
994 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Unit>, String> {
995 Ok(self
996 .units
997 .lock()
998 .unwrap()
999 .values()
1000 .filter(|u| u.building_id == building_id)
1001 .cloned()
1002 .collect())
1003 }
1004
1005 async fn find_by_owner(&self, _owner_id: Uuid) -> Result<Vec<Unit>, String> {
1006 Ok(vec![])
1007 }
1008
1009 async fn find_all_paginated(
1010 &self,
1011 _page_request: &PageRequest,
1012 _filters: &UnitFilters,
1013 ) -> Result<(Vec<Unit>, i64), String> {
1014 let units: Vec<_> = self.units.lock().unwrap().values().cloned().collect();
1015 let total = units.len() as i64;
1016 Ok((units, total))
1017 }
1018
1019 async fn update(&self, unit: &Unit) -> Result<Unit, String> {
1020 self.units.lock().unwrap().insert(unit.id, unit.clone());
1021 Ok(unit.clone())
1022 }
1023
1024 async fn delete(&self, id: Uuid) -> Result<bool, String> {
1025 Ok(self.units.lock().unwrap().remove(&id).is_some())
1026 }
1027 }
1028
1029 struct MockMeetingRepository {
1030 meetings: Mutex<HashMap<Uuid, Meeting>>,
1031 }
1032
1033 impl MockMeetingRepository {
1034 fn new() -> Self {
1035 Self {
1036 meetings: Mutex::new(HashMap::new()),
1037 }
1038 }
1039 }
1040
1041 #[async_trait]
1042 impl MeetingRepository for MockMeetingRepository {
1043 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String> {
1044 self.meetings
1045 .lock()
1046 .unwrap()
1047 .insert(meeting.id, meeting.clone());
1048 Ok(meeting.clone())
1049 }
1050
1051 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String> {
1052 Ok(self.meetings.lock().unwrap().get(&id).cloned())
1053 }
1054
1055 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String> {
1056 Ok(self
1057 .meetings
1058 .lock()
1059 .unwrap()
1060 .values()
1061 .filter(|m| m.building_id == building_id)
1062 .cloned()
1063 .collect())
1064 }
1065
1066 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String> {
1067 self.meetings
1068 .lock()
1069 .unwrap()
1070 .insert(meeting.id, meeting.clone());
1071 Ok(meeting.clone())
1072 }
1073
1074 async fn delete(&self, id: Uuid) -> Result<bool, String> {
1075 Ok(self.meetings.lock().unwrap().remove(&id).is_some())
1076 }
1077
1078 async fn find_all_paginated(
1079 &self,
1080 _page_request: &PageRequest,
1081 _organization_id: Option<Uuid>,
1082 ) -> Result<(Vec<Meeting>, i64), String> {
1083 let meetings: Vec<_> = self.meetings.lock().unwrap().values().cloned().collect();
1084 let total = meetings.len() as i64;
1085 Ok((meetings, total))
1086 }
1087 }
1088
1089 struct MockResolutionRepository {
1090 resolutions: Mutex<HashMap<Uuid, Resolution>>,
1091 dernier_plafonnement: Mutex<Option<serde_json::Value>>,
1095 }
1096
1097 impl MockResolutionRepository {
1098 fn new() -> Self {
1099 Self {
1100 resolutions: Mutex::new(HashMap::new()),
1101 dernier_plafonnement: Mutex::new(None),
1102 }
1103 }
1104 }
1105
1106 #[async_trait]
1107 impl ResolutionRepository for MockResolutionRepository {
1108 async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
1109 self.resolutions
1110 .lock()
1111 .unwrap()
1112 .insert(resolution.id, resolution.clone());
1113 Ok(resolution.clone())
1114 }
1115
1116 async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
1117 Ok(self.resolutions.lock().unwrap().get(&id).cloned())
1118 }
1119
1120 async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
1121 Ok(self
1122 .resolutions
1123 .lock()
1124 .unwrap()
1125 .values()
1126 .filter(|r| r.meeting_id == meeting_id)
1127 .cloned()
1128 .collect())
1129 }
1130
1131 async fn find_by_status(
1132 &self,
1133 status: ResolutionStatus,
1134 ) -> Result<Vec<Resolution>, String> {
1135 Ok(self
1136 .resolutions
1137 .lock()
1138 .unwrap()
1139 .values()
1140 .filter(|r| r.status == status)
1141 .cloned()
1142 .collect())
1143 }
1144
1145 async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
1146 self.resolutions
1147 .lock()
1148 .unwrap()
1149 .insert(resolution.id, resolution.clone());
1150 Ok(resolution.clone())
1151 }
1152
1153 async fn delete(&self, id: Uuid) -> Result<bool, String> {
1154 Ok(self.resolutions.lock().unwrap().remove(&id).is_some())
1155 }
1156
1157 async fn update_vote_counts(
1158 &self,
1159 resolution_id: Uuid,
1160 vote_count_pour: i32,
1161 vote_count_contre: i32,
1162 vote_count_abstention: i32,
1163 total_voting_power_pour: rust_decimal::Decimal,
1164 total_voting_power_contre: rust_decimal::Decimal,
1165 total_voting_power_abstention: rust_decimal::Decimal,
1166 ) -> Result<(), String> {
1167 if let Some(resolution) = self.resolutions.lock().unwrap().get_mut(&resolution_id) {
1168 resolution.vote_count_pour = vote_count_pour;
1169 resolution.vote_count_contre = vote_count_contre;
1170 resolution.vote_count_abstention = vote_count_abstention;
1171 resolution.total_voting_power_pour = total_voting_power_pour;
1172 resolution.total_voting_power_contre = total_voting_power_contre;
1173 resolution.total_voting_power_abstention = total_voting_power_abstention;
1174 }
1175 Ok(())
1176 }
1177
1178 async fn close_voting(
1179 &self,
1180 resolution_id: Uuid,
1181 final_status: ResolutionStatus,
1182 voix_plafonnees: Option<serde_json::Value>,
1183 ) -> Result<(), String> {
1184 *self.dernier_plafonnement.lock().unwrap() = voix_plafonnees;
1187 if let Some(resolution) = self.resolutions.lock().unwrap().get_mut(&resolution_id) {
1188 resolution.status = final_status;
1189 resolution.voted_at = Some(chrono::Utc::now());
1190 }
1191 Ok(())
1192 }
1193
1194 async fn get_meeting_vote_summary(
1195 &self,
1196 meeting_id: Uuid,
1197 ) -> Result<Vec<Resolution>, String> {
1198 self.find_by_meeting_id(meeting_id).await
1199 }
1200 }
1201
1202 struct MockVoteRepository {
1203 votes: Mutex<HashMap<Uuid, Vote>>,
1204 }
1205
1206 impl MockVoteRepository {
1207 fn new() -> Self {
1208 Self {
1209 votes: Mutex::new(HashMap::new()),
1210 }
1211 }
1212 }
1213
1214 #[async_trait]
1215 impl VoteRepository for MockVoteRepository {
1216 async fn create(&self, vote: &Vote) -> Result<Vote, String> {
1217 self.votes.lock().unwrap().insert(vote.id, vote.clone());
1218 Ok(vote.clone())
1219 }
1220
1221 async fn find_by_id(&self, id: Uuid) -> Result<Option<Vote>, String> {
1222 Ok(self.votes.lock().unwrap().get(&id).cloned())
1223 }
1224
1225 async fn find_by_resolution_id(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
1226 Ok(self
1227 .votes
1228 .lock()
1229 .unwrap()
1230 .values()
1231 .filter(|v| v.resolution_id == resolution_id)
1232 .cloned()
1233 .collect())
1234 }
1235
1236 async fn find_by_owner_id(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
1237 Ok(self
1238 .votes
1239 .lock()
1240 .unwrap()
1241 .values()
1242 .filter(|v| v.owner_id == owner_id)
1243 .cloned()
1244 .collect())
1245 }
1246
1247 async fn find_by_resolution_and_unit(
1248 &self,
1249 resolution_id: Uuid,
1250 unit_id: Uuid,
1251 ) -> Result<Option<Vote>, String> {
1252 Ok(self
1253 .votes
1254 .lock()
1255 .unwrap()
1256 .values()
1257 .find(|v| v.resolution_id == resolution_id && v.unit_id == unit_id)
1258 .cloned())
1259 }
1260
1261 async fn has_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
1262 Ok(self
1263 .find_by_resolution_and_unit(resolution_id, unit_id)
1264 .await?
1265 .is_some())
1266 }
1267
1268 async fn update(&self, vote: &Vote) -> Result<Vote, String> {
1269 self.votes.lock().unwrap().insert(vote.id, vote.clone());
1270 Ok(vote.clone())
1271 }
1272
1273 async fn delete(&self, id: Uuid) -> Result<bool, String> {
1274 Ok(self.votes.lock().unwrap().remove(&id).is_some())
1275 }
1276
1277 async fn count_by_resolution_and_choice(
1278 &self,
1279 resolution_id: Uuid,
1280 ) -> Result<(i32, i32, i32), String> {
1281 let votes = self.find_by_resolution_id(resolution_id).await?;
1282 let pour = votes
1283 .iter()
1284 .filter(|v| v.vote_choice == VoteChoice::Pour)
1285 .count() as i32;
1286 let contre = votes
1287 .iter()
1288 .filter(|v| v.vote_choice == VoteChoice::Contre)
1289 .count() as i32;
1290 let abstention = votes
1291 .iter()
1292 .filter(|v| v.vote_choice == VoteChoice::Abstention)
1293 .count() as i32;
1294 Ok((pour, contre, abstention))
1295 }
1296
1297 async fn sum_voting_power_by_resolution(
1298 &self,
1299 resolution_id: Uuid,
1300 ) -> Result<
1301 (
1302 rust_decimal::Decimal,
1303 rust_decimal::Decimal,
1304 rust_decimal::Decimal,
1305 ),
1306 String,
1307 > {
1308 let votes = self.find_by_resolution_id(resolution_id).await?;
1309 let pour: rust_decimal::Decimal = votes
1310 .iter()
1311 .filter(|v| v.vote_choice == VoteChoice::Pour)
1312 .map(|v| v.voting_power)
1313 .sum();
1314 let contre: rust_decimal::Decimal = votes
1315 .iter()
1316 .filter(|v| v.vote_choice == VoteChoice::Contre)
1317 .map(|v| v.voting_power)
1318 .sum();
1319 let abstention: rust_decimal::Decimal = votes
1320 .iter()
1321 .filter(|v| v.vote_choice == VoteChoice::Abstention)
1322 .map(|v| v.voting_power)
1323 .sum();
1324 Ok((pour, contre, abstention))
1325 }
1326
1327 async fn count_proxy_votes_for_mandataire(
1328 &self,
1329 resolution_id: Uuid,
1330 proxy_owner_id: Uuid,
1331 ) -> Result<(i64, rust_decimal::Decimal), String> {
1332 let votes = self.find_by_resolution_id(resolution_id).await?;
1333 let proxy_votes: Vec<_> = votes
1334 .iter()
1335 .filter(|v| v.proxy_owner_id == Some(proxy_owner_id))
1336 .collect();
1337 let count = proxy_votes.len() as i64;
1338 let power: rust_decimal::Decimal = proxy_votes.iter().map(|v| v.voting_power).sum();
1339 Ok((count, power))
1340 }
1341 }
1342
1343 #[tokio::test]
1358 async fn test_art_3_87_la_lecture_avant_cloture_est_deja_plafonnee() {
1359 let resolution_repo = Arc::new(MockResolutionRepository::new());
1360 let vote_repo = Arc::new(MockVoteRepository::new());
1361 let use_cases = ResolutionUseCases::new(
1362 resolution_repo.clone(),
1363 vote_repo.clone(),
1364 Arc::new(MockMeetingRepository::new()),
1365 Arc::new(MockUnitOwnerRepository::new()),
1366 Arc::new(MockUnitRepository::new()),
1367 );
1368
1369 let resolution = Resolution::new(
1370 Uuid::new_v4(),
1371 "RECETTE-Approbation des comptes".to_string(),
1372 "Scénario de recette du 2026-09-04".to_string(),
1373 ResolutionType::Ordinary,
1374 MajorityType::Absolute,
1375 Some(1),
1376 )
1377 .expect("résolution valide");
1378 let resolution_id = resolution.id;
1379 resolution_repo
1380 .create(&resolution)
1381 .await
1382 .expect("résolution enregistrée");
1383
1384 for (voix, choix) in [
1385 (dec!(550), VoteChoice::Pour),
1386 (dec!(250), VoteChoice::Contre),
1387 (dec!(200), VoteChoice::Contre),
1388 ] {
1389 vote_repo
1390 .create(
1391 &Vote::new(
1392 resolution_id,
1393 Uuid::new_v4(),
1394 Uuid::new_v4(),
1395 choix,
1396 voix,
1397 None,
1398 )
1399 .expect("vote valide"),
1400 )
1401 .await
1402 .expect("vote enregistré");
1403 }
1404
1405 let lue = use_cases
1407 .get_resolution(resolution_id)
1408 .await
1409 .expect("lecture réussie")
1410 .expect("résolution trouvée");
1411
1412 assert_eq!(
1413 lue.total_voting_power_pour,
1414 dec!(450),
1415 "Alice doit être ramenée à la somme des autres présents (250 + 200), \
1416 pas servie à ses 550 ‰ bruts"
1417 );
1418 assert_eq!(lue.total_voting_power_contre, dec!(450));
1419 assert_eq!(
1420 lue.pour_percentage(),
1421 50.0,
1422 "50 % n'est pas la majorité absolue : le pourcentage affiché doit \
1423 répondre au résultat que la clôture proclamera"
1424 );
1425 assert_eq!(
1426 lue.status,
1427 ResolutionStatus::Pending,
1428 "plafonner la lecture ne clôt pas la résolution"
1429 );
1430 }
1431
1432 #[tokio::test]
1438 async fn test_la_lecture_plafonnee_est_idempotente_et_ne_persiste_rien() {
1439 let resolution_repo = Arc::new(MockResolutionRepository::new());
1440 let vote_repo = Arc::new(MockVoteRepository::new());
1441 let use_cases = ResolutionUseCases::new(
1442 resolution_repo.clone(),
1443 vote_repo.clone(),
1444 Arc::new(MockMeetingRepository::new()),
1445 Arc::new(MockUnitOwnerRepository::new()),
1446 Arc::new(MockUnitRepository::new()),
1447 );
1448
1449 let resolution = Resolution::new(
1450 Uuid::new_v4(),
1451 "Lecture répétée".to_string(),
1452 "Contrôle d'idempotence".to_string(),
1453 ResolutionType::Ordinary,
1454 MajorityType::Absolute,
1455 Some(1),
1456 )
1457 .expect("résolution valide");
1458 let resolution_id = resolution.id;
1459 resolution_repo.create(&resolution).await.expect("créée");
1460
1461 for (voix, choix) in [
1462 (dec!(550), VoteChoice::Pour),
1463 (dec!(250), VoteChoice::Contre),
1464 (dec!(200), VoteChoice::Contre),
1465 ] {
1466 vote_repo
1467 .create(
1468 &Vote::new(
1469 resolution_id,
1470 Uuid::new_v4(),
1471 Uuid::new_v4(),
1472 choix,
1473 voix,
1474 None,
1475 )
1476 .expect("vote valide"),
1477 )
1478 .await
1479 .expect("vote enregistré");
1480 }
1481
1482 let une = use_cases
1483 .get_resolution(resolution_id)
1484 .await
1485 .unwrap()
1486 .unwrap();
1487 let deux = use_cases
1488 .get_resolution(resolution_id)
1489 .await
1490 .unwrap()
1491 .unwrap();
1492 assert_eq!(une.total_voting_power_pour, deux.total_voting_power_pour);
1493 assert_eq!(deux.total_voting_power_pour, dec!(450));
1494 }
1495
1496 #[tokio::test]
1508 async fn test_art_3_87_la_cloture_plafonne_un_votant_plus_lourd_que_tous_les_autres() {
1509 let resolution_repo = Arc::new(MockResolutionRepository::new());
1510 let vote_repo = Arc::new(MockVoteRepository::new());
1511 let use_cases = ResolutionUseCases::new(
1512 resolution_repo.clone(),
1513 vote_repo.clone(),
1514 Arc::new(MockMeetingRepository::new()),
1515 Arc::new(MockUnitOwnerRepository::new()),
1516 Arc::new(MockUnitRepository::new()),
1517 );
1518
1519 let resolution = Resolution::new(
1520 Uuid::new_v4(),
1521 "Réfection de la toiture".to_string(),
1522 "Travaux de couverture votés en AGO".to_string(),
1523 ResolutionType::Ordinary,
1524 MajorityType::Absolute,
1525 Some(1),
1526 )
1527 .expect("résolution valide");
1528 let resolution_id = resolution.id;
1529 resolution_repo
1530 .create(&resolution)
1531 .await
1532 .expect("résolution enregistrée");
1533
1534 for (voix, _) in [(dec!(600), 0), (dec!(200), 1), (dec!(199), 2)] {
1536 vote_repo
1537 .create(
1538 &Vote::new(
1539 resolution_id,
1540 Uuid::new_v4(),
1541 Uuid::new_v4(),
1542 VoteChoice::Pour,
1543 voix,
1544 None,
1545 )
1546 .expect("vote valide"),
1547 )
1548 .await
1549 .expect("vote enregistré");
1550 }
1551
1552 use_cases
1554 .close_voting(resolution_id, dec!(1000))
1555 .await
1556 .expect("le plafonnement remplace le refus");
1557
1558 let trace = resolution_repo
1561 .dernier_plafonnement
1562 .lock()
1563 .unwrap()
1564 .clone()
1565 .expect("un plafonnement a eu lieu, il doit être tracé");
1566 let ecarts = trace.as_array().expect("un tableau d'écarts");
1567 assert_eq!(
1568 ecarts.len(),
1569 1,
1570 "un seul votant dépassait la somme des autres"
1571 );
1572 assert_eq!(ecarts[0]["voix_brutes"], serde_json::json!(dec!(600)));
1573 assert_eq!(
1574 ecarts[0]["voix_retenues"],
1575 serde_json::json!(dec!(399)),
1576 "ramené à 200 + 199"
1577 );
1578 }
1579
1580 #[tokio::test]
1590 async fn test_art_3_88_une_egalite_apres_plafonnement_nest_pas_une_majorite() {
1591 let resolution_repo = Arc::new(MockResolutionRepository::new());
1592 let vote_repo = Arc::new(MockVoteRepository::new());
1593 let use_cases = ResolutionUseCases::new(
1594 resolution_repo.clone(),
1595 vote_repo.clone(),
1596 Arc::new(MockMeetingRepository::new()),
1597 Arc::new(MockUnitOwnerRepository::new()),
1598 Arc::new(MockUnitRepository::new()),
1599 );
1600
1601 let resolution = Resolution::new(
1602 Uuid::new_v4(),
1603 "Approbation des comptes".to_string(),
1604 "Comptes annuels".to_string(),
1605 ResolutionType::Ordinary,
1606 MajorityType::Absolute,
1607 Some(1),
1608 )
1609 .expect("résolution valide");
1610 let resolution_id = resolution.id;
1611 resolution_repo
1612 .create(&resolution)
1613 .await
1614 .expect("enregistrée");
1615
1616 for (voix, choix) in [
1617 (dec!(550), VoteChoice::Pour),
1618 (dec!(250), VoteChoice::Contre),
1619 (dec!(200), VoteChoice::Contre),
1620 ] {
1621 vote_repo
1622 .create(
1623 &Vote::new(
1624 resolution_id,
1625 Uuid::new_v4(),
1626 Uuid::new_v4(),
1627 choix,
1628 voix,
1629 None,
1630 )
1631 .expect("vote valide"),
1632 )
1633 .await
1634 .expect("vote enregistré");
1635 }
1636
1637 let close = use_cases
1638 .close_voting(resolution_id, dec!(1000))
1639 .await
1640 .expect("la clôture aboutit");
1641
1642 assert_eq!(
1643 close.total_voting_power_pour,
1644 dec!(450),
1645 "Alice est ramenée à la somme des autres"
1646 );
1647 assert_eq!(close.total_voting_power_contre, dec!(450));
1648 assert_eq!(
1649 close.status,
1650 ResolutionStatus::Rejected,
1651 "450 contre 450 : égalité, donc pas de majorité absolue"
1652 );
1653 }
1654
1655 #[tokio::test]
1657 async fn test_art_3_87_une_seance_equilibree_se_cloture() {
1658 let resolution_repo = Arc::new(MockResolutionRepository::new());
1659 let vote_repo = Arc::new(MockVoteRepository::new());
1660 let use_cases = ResolutionUseCases::new(
1661 resolution_repo.clone(),
1662 vote_repo.clone(),
1663 Arc::new(MockMeetingRepository::new()),
1664 Arc::new(MockUnitOwnerRepository::new()),
1665 Arc::new(MockUnitRepository::new()),
1666 );
1667
1668 let resolution = Resolution::new(
1669 Uuid::new_v4(),
1670 "Réfection de la toiture".to_string(),
1671 "Travaux de couverture votés en AGO".to_string(),
1672 ResolutionType::Ordinary,
1673 MajorityType::Absolute,
1674 Some(1),
1675 )
1676 .expect("résolution valide");
1677 let resolution_id = resolution.id;
1678 resolution_repo
1679 .create(&resolution)
1680 .await
1681 .expect("résolution enregistrée");
1682
1683 for voix in [dec!(400), dec!(300), dec!(300)] {
1684 vote_repo
1685 .create(
1686 &Vote::new(
1687 resolution_id,
1688 Uuid::new_v4(),
1689 Uuid::new_v4(),
1690 VoteChoice::Pour,
1691 voix,
1692 None,
1693 )
1694 .expect("vote valide"),
1695 )
1696 .await
1697 .expect("vote enregistré");
1698 }
1699
1700 assert!(use_cases
1701 .close_voting(resolution_id, dec!(1000))
1702 .await
1703 .is_ok());
1704 }
1705
1706 #[tokio::test]
1707 async fn test_create_resolution() {
1708 let resolution_repo = Arc::new(MockResolutionRepository::new());
1709 let vote_repo = Arc::new(MockVoteRepository::new());
1710 let meeting_repo = Arc::new(MockMeetingRepository::new());
1711 let use_cases = ResolutionUseCases::new(
1712 resolution_repo.clone(),
1713 vote_repo,
1714 meeting_repo.clone(),
1715 Arc::new(MockUnitOwnerRepository::new()),
1716 Arc::new(MockUnitRepository::new()),
1717 );
1718
1719 let org_id = Uuid::new_v4();
1720 let building_id = Uuid::new_v4();
1721 let meeting_id = Uuid::new_v4();
1722
1723 let mut meeting = Meeting::new(
1725 Uuid::new_v4(), org_id,
1727 building_id,
1728 MeetingType::Ordinary,
1729 "AGO 2024".to_string(),
1730 None,
1731 Utc::now() + chrono::Duration::days(30),
1732 "Salle des fêtes".to_string(),
1733 )
1734 .unwrap();
1735 meeting.id = meeting_id;
1736 meeting
1738 .validate_quorum(
1739 rust_decimal_macros::dec!(600),
1740 rust_decimal_macros::dec!(1000),
1741 )
1742 .unwrap();
1743 meeting
1748 .add_agenda_item("Approbation des comptes".to_string())
1749 .unwrap();
1750 meeting_repo.create(&meeting).await.unwrap();
1751
1752 let result = use_cases
1753 .create_resolution(
1754 meeting_id,
1755 "Test Resolution".to_string(),
1756 "Description".to_string(),
1757 ResolutionType::Ordinary,
1758 MajorityType::Absolute,
1759 Some(0),
1760 )
1761 .await;
1762
1763 assert!(result.is_ok());
1764 let resolution = result.unwrap();
1765 assert_eq!(resolution.title, "Test Resolution");
1766 assert_eq!(resolution.status, ResolutionStatus::Pending);
1767 }
1768
1769 #[tokio::test]
1770 async fn test_create_resolution_fails_without_quorum() {
1771 let resolution_repo = Arc::new(MockResolutionRepository::new());
1772 let vote_repo = Arc::new(MockVoteRepository::new());
1773 let meeting_repo = Arc::new(MockMeetingRepository::new());
1774 let use_cases = ResolutionUseCases::new(
1775 resolution_repo.clone(),
1776 vote_repo,
1777 meeting_repo.clone(),
1778 Arc::new(MockUnitOwnerRepository::new()),
1779 Arc::new(MockUnitRepository::new()),
1780 );
1781
1782 let org_id = Uuid::new_v4();
1783 let building_id = Uuid::new_v4();
1784 let meeting_id = Uuid::new_v4();
1785
1786 let mut meeting = Meeting::new(
1788 Uuid::new_v4(), org_id,
1790 building_id,
1791 MeetingType::Ordinary,
1792 "AGO 2024".to_string(),
1793 None,
1794 Utc::now() + chrono::Duration::days(30),
1795 "Salle des fêtes".to_string(),
1796 )
1797 .unwrap();
1798 meeting.id = meeting_id;
1799 meeting
1801 .validate_quorum(
1802 rust_decimal_macros::dec!(400),
1803 rust_decimal_macros::dec!(1000),
1804 )
1805 .unwrap();
1806 meeting
1811 .add_agenda_item("Approbation des comptes".to_string())
1812 .unwrap();
1813 meeting_repo.create(&meeting).await.unwrap();
1814
1815 let result = use_cases
1816 .create_resolution(
1817 meeting_id,
1818 "Test Resolution".to_string(),
1819 "Description".to_string(),
1820 ResolutionType::Ordinary,
1821 MajorityType::Absolute,
1822 Some(0),
1823 )
1824 .await;
1825
1826 assert!(result.is_err());
1828 assert!(result.unwrap_err().contains("second convocation"));
1829 }
1830
1831 #[tokio::test]
1838 async fn security_la_puissance_de_vote_vient_du_lot_pas_de_lappelant() {
1839 let resolution_repo = Arc::new(MockResolutionRepository::new());
1840 let vote_repo = Arc::new(MockVoteRepository::new());
1841 let meeting_repo = Arc::new(MockMeetingRepository::new());
1842 let unit_repo = Arc::new(MockUnitRepository::new());
1843 let resolution =
1844 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
1845
1846 let meeting = meeting_repo
1850 .find_by_id(resolution.meeting_id)
1851 .await
1852 .unwrap()
1853 .unwrap();
1854 let unit_id = Uuid::new_v4();
1855 unit_repo.lot(unit_id, meeting.building_id, dec!(42));
1856
1857 let use_cases = ResolutionUseCases::new(
1858 resolution_repo.clone(),
1859 vote_repo.clone(),
1860 meeting_repo.clone(),
1861 Arc::new(MockUnitOwnerRepository::new()),
1862 unit_repo.clone(),
1863 );
1864
1865 let owner_id = Uuid::new_v4();
1866 let vote = use_cases
1867 .cast_vote(
1868 resolution.id,
1869 owner_id,
1870 unit_id,
1871 VoteChoice::Pour,
1872 None,
1873 Some(VoteAuthMethod::Presence),
1874 owner_id,
1876 )
1877 .await
1878 .expect("le vote doit aboutir");
1879
1880 assert_eq!(
1881 vote.voting_power,
1882 dec!(42),
1883 "la voix enregistrée doit être la quotité du lot (Art. 3.87 § 8 CC), \
1884 pas une valeur choisie par l'appelant"
1885 );
1886 }
1887
1888 #[tokio::test]
1895 async fn security_un_lot_dun_autre_immeuble_ne_peut_pas_voter() {
1896 let resolution_repo = Arc::new(MockResolutionRepository::new());
1897 let vote_repo = Arc::new(MockVoteRepository::new());
1898 let meeting_repo = Arc::new(MockMeetingRepository::new());
1899 let unit_repo = Arc::new(MockUnitRepository::new());
1900 let resolution =
1901 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
1902
1903 let lot_etranger = Uuid::new_v4();
1904 unit_repo.lot(lot_etranger, Uuid::new_v4(), dec!(100));
1905
1906 let use_cases = ResolutionUseCases::new(
1907 resolution_repo.clone(),
1908 vote_repo.clone(),
1909 meeting_repo.clone(),
1910 Arc::new(MockUnitOwnerRepository::new()),
1911 unit_repo.clone(),
1912 );
1913
1914 let owner_id = Uuid::new_v4();
1915 let erreur = use_cases
1916 .cast_vote(
1917 resolution.id,
1918 owner_id,
1919 lot_etranger,
1920 VoteChoice::Pour,
1921 None,
1922 Some(VoteAuthMethod::Presence),
1923 owner_id,
1924 )
1925 .await
1926 .expect_err("un lot d'un autre immeuble ne peut pas voter ici");
1927
1928 assert!(
1929 erreur.contains("n'appartient pas à l'immeuble"),
1930 "le refus doit dire POURQUOI, got: {erreur}"
1931 );
1932 }
1933
1934 #[tokio::test]
1940 async fn negative_un_lot_inexistant_ne_peut_pas_voter() {
1941 let resolution_repo = Arc::new(MockResolutionRepository::new());
1942 let vote_repo = Arc::new(MockVoteRepository::new());
1943 let meeting_repo = Arc::new(MockMeetingRepository::new());
1944 let unit_repo = Arc::new(MockUnitRepository::new());
1946 let resolution =
1947 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
1948 *unit_repo.defaut.lock().unwrap() = None;
1949
1950 let use_cases = ResolutionUseCases::new(
1951 resolution_repo.clone(),
1952 vote_repo.clone(),
1953 meeting_repo.clone(),
1954 Arc::new(MockUnitOwnerRepository::new()),
1955 unit_repo.clone(),
1956 );
1957
1958 let owner_id = Uuid::new_v4();
1959 let erreur = use_cases
1960 .cast_vote(
1961 resolution.id,
1962 owner_id,
1963 Uuid::new_v4(),
1964 VoteChoice::Pour,
1965 None,
1966 Some(VoteAuthMethod::Presence),
1967 owner_id,
1968 )
1969 .await
1970 .expect_err("un lot inexistant ne peut pas voter");
1971
1972 assert!(erreur.contains("Lot introuvable"), "got: {erreur}");
1973 }
1974
1975 #[tokio::test]
1976 async fn test_cast_vote_updates_counts() {
1977 let resolution_repo = Arc::new(MockResolutionRepository::new());
1978 let vote_repo = Arc::new(MockVoteRepository::new());
1979 let meeting_repo = Arc::new(MockMeetingRepository::new());
1980 let unit_repo = Arc::new(MockUnitRepository::new());
1981 let use_cases = ResolutionUseCases::new(
1982 resolution_repo.clone(),
1983 vote_repo.clone(),
1984 meeting_repo.clone(),
1985 Arc::new(MockUnitOwnerRepository::new()),
1986 unit_repo.clone(),
1987 );
1988
1989 let org_id = Uuid::new_v4();
1991 let building_id = Uuid::new_v4();
1992 unit_repo.pose_defaut(building_id, dec!(100));
1994 let meeting_id = Uuid::new_v4();
1995
1996 let mut meeting = Meeting::new(
1998 Uuid::new_v4(), org_id,
2000 building_id,
2001 MeetingType::Ordinary,
2002 "AGO 2024".to_string(),
2003 None,
2004 Utc::now() + chrono::Duration::days(30),
2005 "Salle des fêtes".to_string(),
2006 )
2007 .unwrap();
2008 meeting.id = meeting_id;
2009 meeting
2010 .validate_quorum(
2011 rust_decimal_macros::dec!(600),
2012 rust_decimal_macros::dec!(1000),
2013 )
2014 .unwrap();
2015 meeting
2020 .add_agenda_item("Approbation des comptes".to_string())
2021 .unwrap();
2022 meeting_repo.create(&meeting).await.unwrap();
2023
2024 let resolution = use_cases
2025 .create_resolution(
2026 meeting_id,
2027 "Test Resolution".to_string(),
2028 "Description".to_string(),
2029 ResolutionType::Ordinary,
2030 MajorityType::Absolute,
2031 Some(0),
2032 )
2033 .await
2034 .unwrap();
2035
2036 let owner_id = Uuid::new_v4();
2038 let unit_id = Uuid::new_v4();
2039 let result = use_cases
2040 .cast_vote(
2041 resolution.id,
2042 owner_id,
2043 unit_id,
2044 VoteChoice::Pour,
2045 None,
2046 Some(VoteAuthMethod::Presence),
2047 owner_id,
2048 )
2049 .await;
2050
2051 assert!(result.is_ok());
2052
2053 let updated_resolution = use_cases
2055 .get_resolution(resolution.id)
2056 .await
2057 .unwrap()
2058 .unwrap();
2059 assert_eq!(updated_resolution.vote_count_pour, 1);
2060 assert_eq!(
2061 updated_resolution.total_voting_power_pour,
2062 rust_decimal_macros::dec!(100)
2063 );
2064 }
2065
2066 #[tokio::test]
2067 async fn test_cannot_vote_twice() {
2068 let resolution_repo = Arc::new(MockResolutionRepository::new());
2069 let vote_repo = Arc::new(MockVoteRepository::new());
2070 let meeting_repo = Arc::new(MockMeetingRepository::new());
2071 let unit_repo = Arc::new(MockUnitRepository::new());
2072 let use_cases = ResolutionUseCases::new(
2073 resolution_repo.clone(),
2074 vote_repo,
2075 meeting_repo.clone(),
2076 Arc::new(MockUnitOwnerRepository::new()),
2077 unit_repo.clone(),
2078 );
2079
2080 let org_id = Uuid::new_v4();
2082 let building_id = Uuid::new_v4();
2083 unit_repo.pose_defaut(building_id, dec!(100));
2085 let meeting_id = Uuid::new_v4();
2086
2087 let mut meeting = Meeting::new(
2089 Uuid::new_v4(), org_id,
2091 building_id,
2092 MeetingType::Ordinary,
2093 "AGO 2024".to_string(),
2094 None,
2095 Utc::now() + chrono::Duration::days(30),
2096 "Salle des fêtes".to_string(),
2097 )
2098 .unwrap();
2099 meeting.id = meeting_id;
2100 meeting
2101 .validate_quorum(
2102 rust_decimal_macros::dec!(600),
2103 rust_decimal_macros::dec!(1000),
2104 )
2105 .unwrap();
2106 meeting
2111 .add_agenda_item("Approbation des comptes".to_string())
2112 .unwrap();
2113 meeting_repo.create(&meeting).await.unwrap();
2114
2115 let resolution = use_cases
2116 .create_resolution(
2117 meeting_id,
2118 "Test".to_string(),
2119 "Desc".to_string(),
2120 ResolutionType::Ordinary,
2121 MajorityType::Absolute,
2122 Some(0),
2123 )
2124 .await
2125 .unwrap();
2126
2127 let owner_id = Uuid::new_v4();
2128 let unit_id = Uuid::new_v4();
2129
2130 let result1 = use_cases
2132 .cast_vote(
2133 resolution.id,
2134 owner_id,
2135 unit_id,
2136 VoteChoice::Pour,
2137 None,
2138 Some(VoteAuthMethod::Presence),
2139 owner_id,
2140 )
2141 .await;
2142 assert!(result1.is_ok());
2143
2144 let result2 = use_cases
2146 .cast_vote(
2147 resolution.id,
2148 owner_id,
2149 unit_id,
2150 VoteChoice::Contre,
2151 None,
2152 Some(VoteAuthMethod::Presence),
2153 owner_id,
2154 )
2155 .await;
2156 assert!(result2.is_err());
2157 assert!(result2.unwrap_err().contains("already voted"));
2158 }
2159
2160 #[tokio::test]
2162 async fn test_proxy_limit_max_3_enforced() {
2163 let resolution_repo = Arc::new(MockResolutionRepository::new());
2164 let vote_repo = Arc::new(MockVoteRepository::new());
2165 let meeting_repo = Arc::new(MockMeetingRepository::new());
2166 let unit_repo = Arc::new(MockUnitRepository::new());
2167 let use_cases = ResolutionUseCases::new(
2168 resolution_repo.clone(),
2169 vote_repo.clone(),
2170 meeting_repo.clone(),
2171 Arc::new(MockUnitOwnerRepository::new()),
2172 unit_repo.clone(),
2173 );
2174
2175 let org_id = Uuid::new_v4();
2176 let building_id = Uuid::new_v4();
2177 unit_repo.pose_defaut(building_id, dec!(100));
2179 let meeting_id = Uuid::new_v4();
2180
2181 let mut meeting = Meeting::new(
2183 Uuid::new_v4(), org_id,
2185 building_id,
2186 MeetingType::Ordinary,
2187 "AGO 2024".to_string(),
2188 None,
2189 Utc::now() + chrono::Duration::days(30),
2190 "Salle des fêtes".to_string(),
2191 )
2192 .unwrap();
2193 meeting.id = meeting_id;
2194 meeting
2195 .validate_quorum(
2196 rust_decimal_macros::dec!(600),
2197 rust_decimal_macros::dec!(1000),
2198 )
2199 .unwrap();
2200 meeting
2205 .add_agenda_item("Approbation des comptes".to_string())
2206 .unwrap();
2207 meeting_repo.create(&meeting).await.unwrap();
2208
2209 let resolution = use_cases
2210 .create_resolution(
2211 meeting_id,
2212 "Test procurations".to_string(),
2213 "Description".to_string(),
2214 ResolutionType::Ordinary,
2215 MajorityType::Absolute,
2216 Some(0),
2217 )
2218 .await
2219 .unwrap();
2220
2221 let mandataire_id = Uuid::new_v4();
2222
2223 for i in 0..3 {
2225 let owner_id = Uuid::new_v4();
2226 let unit_id = Uuid::new_v4();
2227 let result = use_cases
2228 .cast_vote(
2229 resolution.id,
2230 owner_id,
2231 unit_id,
2232 VoteChoice::Pour, Some(mandataire_id),
2234 Some(VoteAuthMethod::Proxy),
2235 mandataire_id,
2237 )
2238 .await;
2239 assert!(
2240 result.is_ok(),
2241 "Proxy vote {} should succeed, got: {:?}",
2242 i + 1,
2243 result.err()
2244 );
2245 }
2246
2247 let owner_id_4 = Uuid::new_v4();
2249 let unit_id_4 = Uuid::new_v4();
2250 let result4 = use_cases
2251 .cast_vote(
2252 resolution.id,
2253 owner_id_4,
2254 unit_id_4,
2255 VoteChoice::Pour,
2256 Some(mandataire_id),
2257 Some(VoteAuthMethod::Proxy),
2258 mandataire_id,
2259 )
2260 .await;
2261 assert!(result4.is_err(), "4th proxy vote should be rejected");
2262 assert!(
2263 result4.unwrap_err().contains("3"),
2264 "Error should mention the 3-proxy limit"
2265 );
2266 }
2267
2268 #[tokio::test]
2278 async fn edge_la_limite_de_trois_procurations_sapplique_meme_non_declaree() {
2279 let resolution_repo = Arc::new(MockResolutionRepository::new());
2280 let vote_repo = Arc::new(MockVoteRepository::new());
2281 let meeting_repo = Arc::new(MockMeetingRepository::new());
2282 let unit_repo = Arc::new(MockUnitRepository::new());
2283 let use_cases = ResolutionUseCases::new(
2284 resolution_repo.clone(),
2285 vote_repo.clone(),
2286 meeting_repo.clone(),
2287 Arc::new(MockUnitOwnerRepository::new()),
2288 unit_repo.clone(),
2289 );
2290
2291 let org_id = Uuid::new_v4();
2292 let building_id = Uuid::new_v4();
2293 unit_repo.pose_defaut(building_id, dec!(100));
2294 let meeting_id = Uuid::new_v4();
2295
2296 let mut meeting = Meeting::new(
2297 Uuid::new_v4(),
2298 org_id,
2299 building_id,
2300 MeetingType::Ordinary,
2301 "AGO 2024".to_string(),
2302 None,
2303 Utc::now() + chrono::Duration::days(30),
2304 "Salle des fêtes".to_string(),
2305 )
2306 .unwrap();
2307 meeting.id = meeting_id;
2308 meeting
2309 .validate_quorum(
2310 rust_decimal_macros::dec!(600),
2311 rust_decimal_macros::dec!(1000),
2312 )
2313 .unwrap();
2314 meeting
2315 .add_agenda_item("Approbation des comptes".to_string())
2316 .unwrap();
2317 meeting_repo.create(&meeting).await.unwrap();
2318
2319 let resolution = use_cases
2320 .create_resolution(
2321 meeting_id,
2322 "Test contournement procurations".to_string(),
2323 "Description".to_string(),
2324 ResolutionType::Ordinary,
2325 MajorityType::Absolute,
2326 Some(0),
2327 )
2328 .await
2329 .unwrap();
2330
2331 let mandataire_id = Uuid::new_v4();
2332
2333 for _ in 0..3 {
2335 let owner_id = Uuid::new_v4();
2336 let unit_id = Uuid::new_v4();
2337 let result = use_cases
2338 .cast_vote(
2339 resolution.id,
2340 owner_id,
2341 unit_id,
2342 VoteChoice::Pour,
2343 Some(mandataire_id),
2344 Some(VoteAuthMethod::Proxy),
2345 mandataire_id,
2346 )
2347 .await;
2348 assert!(result.is_ok(), "les 3 premières procurations passent");
2349 }
2350
2351 let victime_id = Uuid::new_v4();
2356 let unit_victime = Uuid::new_v4();
2357 let resultat = use_cases
2358 .cast_vote(
2359 resolution.id,
2360 victime_id,
2361 unit_victime,
2362 VoteChoice::Pour,
2363 None,
2364 Some(VoteAuthMethod::Presence),
2365 mandataire_id,
2368 )
2369 .await;
2370
2371 assert!(
2372 resultat.is_err(),
2373 "le 4ᵉ vote non déclaré doit être refusé, pas silencieusement accepté"
2374 );
2375 let erreur = resultat.unwrap_err();
2376 assert!(
2377 erreur.contains("FORBIDDEN"),
2378 "le refus doit être typé usurpation/mandat non déclaré, got: {erreur}"
2379 );
2380 }
2381
2382 #[tokio::test]
2384 async fn test_proxy_limit_10_percent_exception_allows_more() {
2385 let resolution_repo = Arc::new(MockResolutionRepository::new());
2386 let vote_repo = Arc::new(MockVoteRepository::new());
2387 let meeting_repo = Arc::new(MockMeetingRepository::new());
2388 let unit_repo = Arc::new(MockUnitRepository::new());
2389 let use_cases = ResolutionUseCases::new(
2390 resolution_repo.clone(),
2391 vote_repo.clone(),
2392 meeting_repo.clone(),
2393 Arc::new(MockUnitOwnerRepository::new()),
2394 unit_repo.clone(),
2395 );
2396
2397 let org_id = Uuid::new_v4();
2398 let building_id = Uuid::new_v4();
2399 unit_repo.pose_defaut(building_id, dec!(100));
2401 let meeting_id = Uuid::new_v4();
2402
2403 let mut meeting = Meeting::new(
2405 Uuid::new_v4(), org_id,
2407 building_id,
2408 MeetingType::Ordinary,
2409 "AGO 2024".to_string(),
2410 None,
2411 Utc::now() + chrono::Duration::days(30),
2412 "Salle des fêtes".to_string(),
2413 )
2414 .unwrap();
2415 meeting.id = meeting_id;
2416 meeting
2417 .validate_quorum(
2418 rust_decimal_macros::dec!(600),
2419 rust_decimal_macros::dec!(1000),
2420 )
2421 .unwrap();
2422 meeting
2427 .add_agenda_item("Approbation des comptes".to_string())
2428 .unwrap();
2429 meeting_repo.create(&meeting).await.unwrap();
2430
2431 let resolution = use_cases
2432 .create_resolution(
2433 meeting_id,
2434 "Test exception 10%".to_string(),
2435 "Description".to_string(),
2436 ResolutionType::Ordinary,
2437 MajorityType::Absolute,
2438 Some(0),
2439 )
2440 .await
2441 .unwrap();
2442
2443 let mandataire_id = Uuid::new_v4();
2444
2445 for _ in 0..9 {
2447 let owner_id = Uuid::new_v4();
2448 let _ = use_cases
2449 .cast_vote(
2450 resolution.id,
2451 owner_id,
2452 Uuid::new_v4(),
2453 VoteChoice::Pour,
2454 None,
2455 Some(VoteAuthMethod::Presence),
2456 owner_id,
2457 )
2458 .await;
2459 }
2460
2461 for i in 0..4 {
2469 let petit_lot = Uuid::new_v4();
2470 unit_repo.lot(petit_lot, building_id, dec!(5));
2471 let result = use_cases
2472 .cast_vote(
2473 resolution.id,
2474 Uuid::new_v4(),
2475 petit_lot,
2476 VoteChoice::Pour,
2477 Some(mandataire_id),
2478 Some(VoteAuthMethod::Proxy),
2479 mandataire_id,
2480 )
2481 .await;
2482 assert!(
2483 result.is_ok(),
2484 "Proxy vote {} (10% exception) should succeed, got: {:?}",
2485 i + 1,
2486 result.err()
2487 );
2488 }
2489 }
2490
2491 #[tokio::test]
2499 async fn security_cast_vote_rejected_without_quorum() {
2500 let resolution_repo = Arc::new(MockResolutionRepository::new());
2501 let vote_repo = Arc::new(MockVoteRepository::new());
2502 let meeting_repo = Arc::new(MockMeetingRepository::new());
2503 let use_cases = ResolutionUseCases::new(
2504 resolution_repo.clone(),
2505 vote_repo,
2506 meeting_repo.clone(),
2507 Arc::new(MockUnitOwnerRepository::new()),
2508 Arc::new(MockUnitRepository::new()),
2509 );
2510
2511 let meeting_id = Uuid::new_v4();
2512 let mut meeting = Meeting::new(
2513 Uuid::new_v4(), Uuid::new_v4(),
2515 Uuid::new_v4(),
2516 MeetingType::Ordinary,
2517 "AGO sans quorum".to_string(),
2518 None,
2519 Utc::now() + chrono::Duration::days(30),
2520 "Salle".to_string(),
2521 )
2522 .unwrap();
2523 meeting.id = meeting_id;
2524 meeting
2530 .add_agenda_item("Approbation des comptes".to_string())
2531 .unwrap();
2532 meeting_repo.create(&meeting).await.unwrap();
2533
2534 let mut resolution = Resolution::new(
2535 meeting_id,
2536 "R".to_string(),
2537 "D".to_string(),
2538 ResolutionType::Ordinary,
2539 MajorityType::Absolute,
2540 Some(0),
2545 )
2546 .unwrap();
2547 resolution.status = ResolutionStatus::Pending;
2548 resolution_repo.create(&resolution).await.unwrap();
2549
2550 let owner_id = Uuid::new_v4();
2551 let err = use_cases
2552 .cast_vote(
2553 resolution.id,
2554 owner_id,
2555 Uuid::new_v4(),
2556 VoteChoice::Pour,
2557 None,
2558 Some(VoteAuthMethod::Presence),
2559 owner_id,
2560 )
2561 .await
2562 .unwrap_err();
2563 assert!(
2564 err.contains("Quorum"),
2565 "le vote sans quorum doit être rejeté, got: {}",
2566 err
2567 );
2568 }
2569
2570 #[tokio::test]
2573 async fn negative_cast_vote_meeting_not_found() {
2574 let resolution_repo = Arc::new(MockResolutionRepository::new());
2575 let vote_repo = Arc::new(MockVoteRepository::new());
2576 let meeting_repo = Arc::new(MockMeetingRepository::new()); let use_cases = ResolutionUseCases::new(
2578 resolution_repo.clone(),
2579 vote_repo,
2580 meeting_repo,
2581 Arc::new(MockUnitOwnerRepository::new()),
2582 Arc::new(MockUnitRepository::new()),
2583 );
2584
2585 let mut resolution = Resolution::new(
2586 Uuid::new_v4(), "R".to_string(),
2588 "D".to_string(),
2589 ResolutionType::Ordinary,
2590 MajorityType::Absolute,
2591 Some(0),
2595 )
2596 .unwrap();
2597 resolution.status = ResolutionStatus::Pending;
2598 resolution_repo.create(&resolution).await.unwrap();
2599
2600 let owner_id = Uuid::new_v4();
2601 let err = use_cases
2602 .cast_vote(
2603 resolution.id,
2604 owner_id,
2605 Uuid::new_v4(),
2606 VoteChoice::Pour,
2607 None,
2608 Some(VoteAuthMethod::Presence),
2609 owner_id,
2610 )
2611 .await
2612 .unwrap_err();
2613 assert!(
2614 err.contains("Meeting not found"),
2615 "réunion absente → erreur typée, got: {}",
2616 err
2617 );
2618 }
2619
2620 async fn meeting_with_quorum_and_resolution(
2634 resolution_repo: &Arc<MockResolutionRepository>,
2635 meeting_repo: &Arc<MockMeetingRepository>,
2636 unit_repo: &Arc<MockUnitRepository>,
2637 ) -> Resolution {
2638 let meeting_id = Uuid::new_v4();
2639 let building_id = Uuid::new_v4();
2640 unit_repo.pose_defaut(building_id, dec!(100));
2641 let mut meeting = Meeting::new(
2642 Uuid::new_v4(), Uuid::new_v4(),
2644 building_id,
2645 MeetingType::Ordinary,
2646 "AGO".to_string(),
2647 None,
2648 Utc::now() + chrono::Duration::days(30),
2649 "Salle".to_string(),
2650 )
2651 .unwrap();
2652 meeting.id = meeting_id;
2653 meeting
2654 .validate_quorum(
2655 rust_decimal_macros::dec!(600),
2656 rust_decimal_macros::dec!(1000),
2657 )
2658 .unwrap();
2659 meeting
2664 .add_agenda_item("Approbation des comptes".to_string())
2665 .unwrap();
2666 meeting_repo.create(&meeting).await.unwrap();
2667
2668 let mut resolution = Resolution::new(
2669 meeting_id,
2670 "R".to_string(),
2671 "D".to_string(),
2672 ResolutionType::Ordinary,
2673 MajorityType::Absolute,
2674 Some(0),
2678 )
2679 .unwrap();
2680 resolution.status = ResolutionStatus::Pending;
2681 resolution_repo.create(&resolution).await.unwrap();
2682 resolution
2683 }
2684
2685 async fn meeting_with_quorum_and_resolution_remote(
2689 resolution_repo: &Arc<MockResolutionRepository>,
2690 meeting_repo: &Arc<MockMeetingRepository>,
2691 unit_repo: &Arc<MockUnitRepository>,
2692 ) -> Resolution {
2693 let meeting_id = Uuid::new_v4();
2694 let building_id = Uuid::new_v4();
2695 unit_repo.pose_defaut(building_id, dec!(100));
2696 let mut meeting = Meeting::new(
2697 Uuid::new_v4(), Uuid::new_v4(),
2699 building_id,
2700 MeetingType::Ordinary,
2701 "AGO distancielle".to_string(),
2702 None,
2703 Utc::now() + chrono::Duration::days(30),
2704 "Visioconférence".to_string(),
2705 )
2706 .unwrap();
2707 meeting.id = meeting_id;
2708 meeting
2709 .set_mode(
2710 MeetingMode::Remote,
2711 Some("https://visio.example/ago".to_string()),
2712 )
2713 .unwrap();
2714 meeting
2715 .validate_quorum(
2716 rust_decimal_macros::dec!(600),
2717 rust_decimal_macros::dec!(1000),
2718 )
2719 .unwrap();
2720 meeting
2721 .add_agenda_item("Approbation des comptes".to_string())
2722 .unwrap();
2723 meeting_repo.create(&meeting).await.unwrap();
2724
2725 let mut resolution = Resolution::new(
2726 meeting_id,
2727 "R".to_string(),
2728 "D".to_string(),
2729 ResolutionType::Ordinary,
2730 MajorityType::Absolute,
2731 Some(0),
2732 )
2733 .unwrap();
2734 resolution.status = ResolutionStatus::Pending;
2735 resolution_repo.create(&resolution).await.unwrap();
2736 resolution
2737 }
2738
2739 #[tokio::test]
2746 async fn happy_vote_distant_avec_itsme_est_enregistre() {
2747 let resolution_repo = Arc::new(MockResolutionRepository::new());
2748 let vote_repo = Arc::new(MockVoteRepository::new());
2749 let meeting_repo = Arc::new(MockMeetingRepository::new());
2750 let unit_repo = Arc::new(MockUnitRepository::new());
2751 let resolution =
2752 meeting_with_quorum_and_resolution_remote(&resolution_repo, &meeting_repo, &unit_repo)
2753 .await;
2754 let use_cases = ResolutionUseCases::new(
2755 resolution_repo.clone(),
2756 vote_repo,
2757 meeting_repo,
2758 Arc::new(MockUnitOwnerRepository::new()),
2759 unit_repo,
2760 );
2761
2762 let owner_id = Uuid::new_v4();
2763 let unit_id = Uuid::new_v4();
2764 let vote = use_cases
2765 .cast_vote(
2766 resolution.id,
2767 owner_id,
2768 unit_id,
2769 VoteChoice::Pour,
2770 None,
2771 Some(VoteAuthMethod::Itsme),
2772 owner_id,
2773 )
2774 .await
2775 .expect("un vote itsme sur une AG distancielle doit aboutir");
2776
2777 assert_eq!(vote.auth_method, VoteAuthMethod::Itsme);
2778 }
2779
2780 #[tokio::test]
2784 async fn edge_vote_distant_par_procuration_est_autorise() {
2785 let resolution_repo = Arc::new(MockResolutionRepository::new());
2786 let vote_repo = Arc::new(MockVoteRepository::new());
2787 let meeting_repo = Arc::new(MockMeetingRepository::new());
2788 let unit_repo = Arc::new(MockUnitRepository::new());
2789 let resolution =
2790 meeting_with_quorum_and_resolution_remote(&resolution_repo, &meeting_repo, &unit_repo)
2791 .await;
2792 let use_cases = ResolutionUseCases::new(
2793 resolution_repo.clone(),
2794 vote_repo,
2795 meeting_repo,
2796 Arc::new(MockUnitOwnerRepository::new()),
2797 unit_repo,
2798 );
2799
2800 let mandant_id = Uuid::new_v4();
2801 let mandataire_id = Uuid::new_v4();
2802 let unit_id = Uuid::new_v4();
2803 let vote = use_cases
2804 .cast_vote(
2805 resolution.id,
2806 mandant_id,
2807 unit_id,
2808 VoteChoice::Pour,
2809 Some(mandataire_id),
2810 Some(VoteAuthMethod::Proxy),
2811 mandataire_id,
2812 )
2813 .await
2814 .expect("une procuration réelle doit être autorisée à distance");
2815
2816 assert_eq!(vote.auth_method, VoteAuthMethod::Proxy);
2817 }
2818
2819 #[tokio::test]
2824 async fn security_vote_distant_avec_presence_est_refuse_403() {
2825 let resolution_repo = Arc::new(MockResolutionRepository::new());
2826 let vote_repo = Arc::new(MockVoteRepository::new());
2827 let meeting_repo = Arc::new(MockMeetingRepository::new());
2828 let unit_repo = Arc::new(MockUnitRepository::new());
2829 let resolution =
2830 meeting_with_quorum_and_resolution_remote(&resolution_repo, &meeting_repo, &unit_repo)
2831 .await;
2832 let use_cases = ResolutionUseCases::new(
2833 resolution_repo.clone(),
2834 vote_repo,
2835 meeting_repo,
2836 Arc::new(MockUnitOwnerRepository::new()),
2837 unit_repo,
2838 );
2839
2840 let owner_id = Uuid::new_v4();
2841 let unit_id = Uuid::new_v4();
2842 let erreur = use_cases
2843 .cast_vote(
2844 resolution.id,
2845 owner_id,
2846 unit_id,
2847 VoteChoice::Pour,
2848 None,
2849 Some(VoteAuthMethod::Presence),
2850 owner_id,
2851 )
2852 .await
2853 .expect_err("presence ne doit pas suffire à distance");
2854
2855 assert_eq!(
2856 erreur, "VOTE_AUTH_INSUFFICIENT:remote:presence",
2857 "le refus doit être typé, parsable par le handler : {erreur}"
2858 );
2859 let app_err: crate::application::error::AppError =
2860 crate::domain::entities::VoteAuthError::Insufficient {
2861 mode: MeetingMode::Remote,
2862 auth_method: VoteAuthMethod::Presence,
2863 }
2864 .into();
2865 assert_eq!(
2866 app_err.status_code(),
2867 actix_web::http::StatusCode::FORBIDDEN
2868 );
2869 }
2870
2871 #[tokio::test]
2874 async fn negative_vote_sans_auth_method_est_refuse_422() {
2875 let resolution_repo = Arc::new(MockResolutionRepository::new());
2876 let vote_repo = Arc::new(MockVoteRepository::new());
2877 let meeting_repo = Arc::new(MockMeetingRepository::new());
2878 let unit_repo = Arc::new(MockUnitRepository::new());
2879 let resolution =
2880 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
2881 let use_cases = ResolutionUseCases::new(
2882 resolution_repo.clone(),
2883 vote_repo,
2884 meeting_repo,
2885 Arc::new(MockUnitOwnerRepository::new()),
2886 unit_repo,
2887 );
2888
2889 let owner_id = Uuid::new_v4();
2890 let unit_id = Uuid::new_v4();
2891 let erreur = use_cases
2892 .cast_vote(
2893 resolution.id,
2894 owner_id,
2895 unit_id,
2896 VoteChoice::Pour,
2897 None,
2898 None,
2899 owner_id,
2900 )
2901 .await
2902 .expect_err("un vote sans auth_method ne peut pas être enregistré");
2903
2904 assert_eq!(
2905 erreur, "VOTE_AUTH_METHOD_REQUIRED",
2906 "le refus doit être typé, parsable par le handler : {erreur}"
2907 );
2908 let app_err: crate::application::error::AppError =
2909 crate::domain::entities::VoteAuthError::Missing.into();
2910 assert_eq!(
2911 app_err.status_code(),
2912 actix_web::http::StatusCode::UNPROCESSABLE_ENTITY
2913 );
2914 }
2915
2916 #[tokio::test]
2927 async fn security_vote_refuse_sur_resolution_hors_ordre_du_jour() {
2928 let resolution_repo = Arc::new(MockResolutionRepository::new());
2929 let vote_repo = Arc::new(MockVoteRepository::new());
2930 let meeting_repo = Arc::new(MockMeetingRepository::new());
2931 let unit_repo = Arc::new(MockUnitRepository::new());
2932 let resolution =
2933 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
2934
2935 let mut orpheline = resolution.clone();
2937 orpheline.agenda_item_index = None;
2938 resolution_repo.create(&orpheline).await.unwrap();
2939
2940 let unit_id = Uuid::new_v4();
2941 let unit_owner_repo = Arc::new(MockUnitOwnerRepository::with_holders(
2942 unit_id,
2943 vec![LotHolder::new(OwnershipType::FullOwner, false)],
2944 ));
2945 let use_cases = ResolutionUseCases::new(
2946 resolution_repo.clone(),
2947 vote_repo,
2948 meeting_repo,
2949 unit_owner_repo,
2950 Arc::new(MockUnitRepository::new()),
2952 );
2953
2954 let owner_id = Uuid::new_v4();
2955 let erreur = use_cases
2956 .cast_vote(
2957 orpheline.id,
2958 owner_id,
2959 unit_id,
2960 VoteChoice::Pour,
2961 None,
2962 Some(VoteAuthMethod::Presence),
2963 owner_id,
2964 )
2965 .await
2966 .expect_err("une résolution hors ordre du jour ne doit pas être votable");
2967
2968 assert!(
2969 erreur.contains("ordre du jour") && erreur.contains("3.87"),
2970 "le refus doit citer l'ordre du jour et l'article : {erreur}"
2971 );
2972 assert!(
2973 erreur.contains("nulle"),
2974 "le refus doit dire POURQUOI — la décision serait nulle : {erreur}"
2975 );
2976 }
2977
2978 #[tokio::test]
2985 async fn security_vote_refuse_sur_point_inexistant() {
2986 let resolution_repo = Arc::new(MockResolutionRepository::new());
2987 let vote_repo = Arc::new(MockVoteRepository::new());
2988 let meeting_repo = Arc::new(MockMeetingRepository::new());
2989 let unit_repo = Arc::new(MockUnitRepository::new());
2990 let resolution =
2991 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
2992
2993 let mut hors_bornes = resolution.clone();
2995 hors_bornes.agenda_item_index = Some(7);
2996 resolution_repo.create(&hors_bornes).await.unwrap();
2997
2998 let unit_id = Uuid::new_v4();
2999 let unit_owner_repo = Arc::new(MockUnitOwnerRepository::with_holders(
3000 unit_id,
3001 vec![LotHolder::new(OwnershipType::FullOwner, false)],
3002 ));
3003 let use_cases = ResolutionUseCases::new(
3004 resolution_repo.clone(),
3005 vote_repo,
3006 meeting_repo,
3007 unit_owner_repo,
3008 Arc::new(MockUnitRepository::new()),
3010 );
3011
3012 let owner_id = Uuid::new_v4();
3013 let erreur = use_cases
3014 .cast_vote(
3015 hors_bornes.id,
3016 owner_id,
3017 unit_id,
3018 VoteChoice::Pour,
3019 None,
3020 Some(VoteAuthMethod::Presence),
3021 owner_id,
3022 )
3023 .await
3024 .expect_err("un point d'ordre du jour inexistant ne doit pas être votable");
3025
3026 assert!(
3027 erreur.contains("n'existe pas") && erreur.contains("3.87"),
3028 "le refus doit dire que le point n'existe pas, et citer l'article : {erreur}"
3029 );
3030 }
3031
3032 #[tokio::test]
3036 async fn security_vote_refuse_sur_point_dordre_du_jour_vide() {
3037 let resolution_repo = Arc::new(MockResolutionRepository::new());
3038 let vote_repo = Arc::new(MockVoteRepository::new());
3039 let meeting_repo = Arc::new(MockMeetingRepository::new());
3040 let unit_repo = Arc::new(MockUnitRepository::new());
3041 let resolution =
3042 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
3043
3044 let mut reunion = meeting_repo
3046 .find_by_id(resolution.meeting_id)
3047 .await
3048 .unwrap()
3049 .expect("l'assemblée du helper doit exister");
3050 reunion.agenda = vec![" ".to_string()];
3051 meeting_repo.update(&reunion).await.unwrap();
3052
3053 let unit_id = Uuid::new_v4();
3054 let unit_owner_repo = Arc::new(MockUnitOwnerRepository::with_holders(
3055 unit_id,
3056 vec![LotHolder::new(OwnershipType::FullOwner, false)],
3057 ));
3058 let use_cases = ResolutionUseCases::new(
3059 resolution_repo.clone(),
3060 vote_repo,
3061 meeting_repo,
3062 unit_owner_repo,
3063 Arc::new(MockUnitRepository::new()),
3065 );
3066
3067 let owner_id = Uuid::new_v4();
3068 let erreur = use_cases
3069 .cast_vote(
3070 resolution.id,
3071 owner_id,
3072 unit_id,
3073 VoteChoice::Pour,
3074 None,
3075 Some(VoteAuthMethod::Presence),
3076 owner_id,
3077 )
3078 .await
3079 .expect_err("un point vide ne doit pas être votable");
3080
3081 assert!(
3082 erreur.contains("vide") && erreur.contains("3.87"),
3083 "le refus doit dire que le point est vide, et citer l'article : {erreur}"
3084 );
3085 }
3086
3087 #[tokio::test]
3091 async fn security_cast_vote_rejected_when_voting_right_suspended() {
3092 let resolution_repo = Arc::new(MockResolutionRepository::new());
3093 let vote_repo = Arc::new(MockVoteRepository::new());
3094 let meeting_repo = Arc::new(MockMeetingRepository::new());
3095 let unit_repo = Arc::new(MockUnitRepository::new());
3096 let resolution =
3097 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
3098
3099 let unit_id = Uuid::new_v4();
3100 let unit_owner_repo = Arc::new(MockUnitOwnerRepository::with_holders(
3102 unit_id,
3103 vec![
3104 LotHolder::new(OwnershipType::Indivisaire, false),
3105 LotHolder::new(OwnershipType::Indivisaire, false),
3106 ],
3107 ));
3108 let use_cases = ResolutionUseCases::new(
3109 resolution_repo.clone(),
3110 vote_repo,
3111 meeting_repo,
3112 unit_owner_repo,
3113 Arc::new(MockUnitRepository::new()),
3115 );
3116
3117 let owner_id = Uuid::new_v4();
3118 let err = use_cases
3119 .cast_vote(
3120 resolution.id,
3121 owner_id,
3122 unit_id,
3123 VoteChoice::Pour,
3124 None,
3125 Some(VoteAuthMethod::Presence),
3126 owner_id,
3127 )
3128 .await
3129 .unwrap_err();
3130 assert!(
3131 err.contains("VOTING_RIGHT_SUSPENDED"),
3132 "lot suspendu → vote rejeté typé, got: {}",
3133 err
3134 );
3135 }
3136
3137 #[tokio::test]
3140 async fn happy_cast_vote_allowed_with_designated_representative() {
3141 let resolution_repo = Arc::new(MockResolutionRepository::new());
3142 let vote_repo = Arc::new(MockVoteRepository::new());
3143 let meeting_repo = Arc::new(MockMeetingRepository::new());
3144 let unit_repo = Arc::new(MockUnitRepository::new());
3145 let resolution =
3146 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
3147
3148 let unit_id = Uuid::new_v4();
3149 let unit_owner_repo = Arc::new(MockUnitOwnerRepository::with_holders(
3151 unit_id,
3152 vec![
3153 LotHolder::new(OwnershipType::Usufruct, true),
3154 LotHolder::new(OwnershipType::BareOwner, false),
3155 ],
3156 ));
3157 let use_cases = ResolutionUseCases::new(
3158 resolution_repo.clone(),
3159 vote_repo,
3160 meeting_repo,
3161 unit_owner_repo,
3162 unit_repo.clone(),
3164 );
3165
3166 let owner_id = Uuid::new_v4();
3167 let result = use_cases
3168 .cast_vote(
3169 resolution.id,
3170 owner_id,
3171 unit_id,
3172 VoteChoice::Pour,
3173 None,
3174 Some(VoteAuthMethod::Presence),
3175 owner_id,
3176 )
3177 .await;
3178 assert!(
3179 result.is_ok(),
3180 "lot avec représentant désigné doit pouvoir voter, got: {:?}",
3181 result.err()
3182 );
3183 }
3184
3185 #[tokio::test]
3190 async fn negative_voter_au_nom_dautrui_sans_procuration_est_refuse() {
3191 let resolution_repo = Arc::new(MockResolutionRepository::new());
3192 let vote_repo = Arc::new(MockVoteRepository::new());
3193 let meeting_repo = Arc::new(MockMeetingRepository::new());
3194 let unit_repo = Arc::new(MockUnitRepository::new());
3195 let resolution =
3196 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
3197 let use_cases = ResolutionUseCases::new(
3198 resolution_repo.clone(),
3199 vote_repo.clone(),
3200 meeting_repo.clone(),
3201 Arc::new(MockUnitOwnerRepository::new()),
3202 unit_repo.clone(),
3203 );
3204
3205 let victime_id = Uuid::new_v4();
3206 let attaquant_id = Uuid::new_v4();
3207 let unit_id = Uuid::new_v4();
3208
3209 let resultat = use_cases
3210 .cast_vote(
3211 resolution.id,
3212 victime_id, unit_id,
3214 VoteChoice::Pour,
3215 None, Some(VoteAuthMethod::Presence),
3217 attaquant_id, )
3219 .await;
3220
3221 assert!(
3222 resultat.is_err(),
3223 "voter au nom d'autrui sans procuration doit être refusé"
3224 );
3225 let erreur = resultat.unwrap_err();
3226 assert!(
3227 erreur.contains("FORBIDDEN"),
3228 "le refus doit être typé (403), pas une chaîne quelconque : {erreur}"
3229 );
3230
3231 assert!(
3234 !vote_repo
3235 .has_voted(resolution.id, unit_id)
3236 .await
3237 .expect("lecture du dépôt"),
3238 "aucun vote ne doit être enregistré après un refus d'usurpation"
3239 );
3240 }
3241
3242 #[tokio::test]
3245 async fn negative_se_declarer_mandataire_dun_tiers_est_refuse() {
3246 let resolution_repo = Arc::new(MockResolutionRepository::new());
3247 let vote_repo = Arc::new(MockVoteRepository::new());
3248 let meeting_repo = Arc::new(MockMeetingRepository::new());
3249 let unit_repo = Arc::new(MockUnitRepository::new());
3250 let resolution =
3251 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
3252 let use_cases = ResolutionUseCases::new(
3253 resolution_repo.clone(),
3254 vote_repo.clone(),
3255 meeting_repo.clone(),
3256 Arc::new(MockUnitOwnerRepository::new()),
3257 unit_repo.clone(),
3258 );
3259
3260 let owner_id = Uuid::new_v4();
3261 let appelant_id = Uuid::new_v4();
3262 let quelquun_dautre = Uuid::new_v4();
3263
3264 let resultat = use_cases
3265 .cast_vote(
3266 resolution.id,
3267 owner_id,
3268 Uuid::new_v4(),
3269 VoteChoice::Pour,
3270 Some(quelquun_dautre), Some(VoteAuthMethod::Presence),
3272 appelant_id,
3273 )
3274 .await;
3275
3276 assert!(
3277 resultat.is_err(),
3278 "une procuration déclarée pour un tiers doit être refusée"
3279 );
3280 assert!(resultat.unwrap_err().contains("FORBIDDEN"));
3281 }
3282
3283 #[tokio::test]
3287 async fn negative_voter_pour_un_lot_quon_ne_detient_pas_est_refuse() {
3288 let resolution_repo = Arc::new(MockResolutionRepository::new());
3289 let vote_repo = Arc::new(MockVoteRepository::new());
3290 let meeting_repo = Arc::new(MockMeetingRepository::new());
3291 let unit_repo = Arc::new(MockUnitRepository::new());
3292 let resolution =
3293 meeting_with_quorum_and_resolution(&resolution_repo, &meeting_repo, &unit_repo).await;
3294
3295 let owner_id = Uuid::new_v4();
3296 let unit_id = Uuid::new_v4();
3297 let unit_owner_repo = Arc::new(MockUnitOwnerRepository::new());
3298 unit_owner_repo.refuse_detention(unit_id, owner_id);
3299
3300 let use_cases = ResolutionUseCases::new(
3301 resolution_repo.clone(),
3302 vote_repo.clone(),
3303 meeting_repo.clone(),
3304 unit_owner_repo,
3305 unit_repo.clone(),
3306 );
3307
3308 let resultat = use_cases
3309 .cast_vote(
3310 resolution.id,
3311 owner_id,
3312 unit_id,
3313 VoteChoice::Pour,
3314 None,
3315 Some(VoteAuthMethod::Presence),
3316 owner_id,
3317 )
3318 .await;
3319
3320 assert!(
3321 resultat.is_err(),
3322 "voter pour un lot qu'on ne détient pas doit être refusé"
3323 );
3324 let erreur = resultat.unwrap_err();
3325 assert!(
3326 erreur.contains("FORBIDDEN") && erreur.contains("ne détient pas"),
3327 "le refus doit dire POURQUOI, got: {erreur}"
3328 );
3329 }
3330
3331 fn build_use_cases_pour_resolution(
3336 resolution_repo: Arc<MockResolutionRepository>,
3337 vote_repo: Arc<MockVoteRepository>,
3338 meeting_repo: Arc<MockMeetingRepository>,
3339 ) -> ResolutionUseCases {
3340 ResolutionUseCases::new(
3341 resolution_repo,
3342 vote_repo,
3343 meeting_repo,
3344 Arc::new(MockUnitOwnerRepository::new()),
3345 Arc::new(MockUnitRepository::new()),
3346 )
3347 }
3348
3349 #[tokio::test]
3355 async fn security_delete_resolution_auto_generated_refuse() {
3356 let resolution_repo = Arc::new(MockResolutionRepository::new());
3357 let auto = Resolution::new_evaluation_contractors_auto(Uuid::new_v4());
3358 resolution_repo.create(&auto).await.unwrap();
3359 let use_cases = build_use_cases_pour_resolution(
3360 resolution_repo,
3361 Arc::new(MockVoteRepository::new()),
3362 Arc::new(MockMeetingRepository::new()),
3363 );
3364
3365 let result = use_cases.delete_resolution(auto.id).await;
3366
3367 assert!(matches!(result, Err(AppError::ResolutionAutoNotRemovable)));
3368 }
3369
3370 #[tokio::test]
3374 async fn negative_update_resolution_auto_generated_refuse() {
3375 let use_cases = build_use_cases_pour_resolution(
3376 Arc::new(MockResolutionRepository::new()),
3377 Arc::new(MockVoteRepository::new()),
3378 Arc::new(MockMeetingRepository::new()),
3379 );
3380 let mut auto = Resolution::new_evaluation_contractors_auto(Uuid::new_v4());
3381 auto.description = "Texte modifié par le syndic".to_string();
3382
3383 let result = use_cases.update_resolution(&auto).await;
3384
3385 assert!(matches!(result, Err(AppError::ResolutionAutoNotRemovable)));
3386 }
3387
3388 #[tokio::test]
3391 async fn happy_delete_resolution_standard_sans_votes_reussit() {
3392 let resolution_repo = Arc::new(MockResolutionRepository::new());
3393 let standard = Resolution::new(
3394 Uuid::new_v4(),
3395 "Titre".to_string(),
3396 "Description".to_string(),
3397 ResolutionType::Ordinary,
3398 MajorityType::Absolute,
3399 None,
3400 )
3401 .unwrap();
3402 resolution_repo.create(&standard).await.unwrap();
3403 let use_cases = build_use_cases_pour_resolution(
3404 resolution_repo,
3405 Arc::new(MockVoteRepository::new()),
3406 Arc::new(MockMeetingRepository::new()),
3407 );
3408
3409 let result = use_cases.delete_resolution(standard.id).await;
3410
3411 assert!(result.unwrap());
3412 }
3413
3414 #[tokio::test]
3417 async fn edge_delete_resolution_introuvable_rend_404() {
3418 let use_cases = build_use_cases_pour_resolution(
3419 Arc::new(MockResolutionRepository::new()),
3420 Arc::new(MockVoteRepository::new()),
3421 Arc::new(MockMeetingRepository::new()),
3422 );
3423
3424 let result = use_cases.delete_resolution(Uuid::new_v4()).await;
3425
3426 assert!(matches!(result, Err(AppError::NotFound(_))));
3427 }
3428}