koprogo_api/domain/copropriete/
requete_ag.rs1use chrono::{DateTime, Duration, Utc};
30use rust_decimal::Decimal;
31use uuid::Uuid;
32
33pub const DELAI_CONVOCATION_JOURS: i64 = 30;
35
36#[derive(Debug, Clone, PartialEq)]
38pub enum RequeteIrrecevable {
39 SeuilNonAtteint {
41 quotites_reunies: Decimal,
42 seuil_requis: Decimal,
43 },
44 SansSignataire,
46}
47
48impl std::fmt::Display for RequeteIrrecevable {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::SeuilNonAtteint {
52 quotites_reunies,
53 seuil_requis,
54 } => write!(
55 f,
56 "Art. 3.87 § 2 : les cosignataires réunissent {quotites_reunies} quotités, \
57 le cinquième des parts en exige {seuil_requis}."
58 ),
59 Self::SansSignataire => write!(
60 f,
61 "Art. 3.87 § 2 : une requête d'assemblée doit émaner d'au moins un \
62 copropriétaire."
63 ),
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct RequeteAg {
71 pub cosignataires: Vec<Uuid>,
72 pub quotites_reunies: Decimal,
73 pub recue_le: DateTime<Utc>,
76 pub echeance_convocation: DateTime<Utc>,
77}
78
79pub fn seuil_du_cinquieme(total_des_lots: Decimal) -> Decimal {
81 total_des_lots / Decimal::from(5)
82}
83
84pub fn deposer(
89 cosignataires: Vec<Uuid>,
90 quotites_reunies: Decimal,
91 total_des_lots: Decimal,
92 recue_le: DateTime<Utc>,
93) -> Result<RequeteAg, RequeteIrrecevable> {
94 if cosignataires.is_empty() {
95 return Err(RequeteIrrecevable::SansSignataire);
96 }
97 let seuil = seuil_du_cinquieme(total_des_lots);
98 if quotites_reunies < seuil {
99 return Err(RequeteIrrecevable::SeuilNonAtteint {
100 quotites_reunies,
101 seuil_requis: seuil,
102 });
103 }
104 Ok(RequeteAg {
105 cosignataires,
106 quotites_reunies,
107 recue_le,
108 echeance_convocation: recue_le + Duration::days(DELAI_CONVOCATION_JOURS),
109 })
110}
111
112impl RequeteAg {
113 pub fn syndic_defaillant(
118 &self,
119 convoquee_le: Option<DateTime<Utc>>,
120 moment: DateTime<Utc>,
121 ) -> bool {
122 match convoquee_le {
123 Some(date) => date > self.echeance_convocation,
124 None => moment > self.echeance_convocation,
125 }
126 }
127
128 pub fn peut_convoquer_lui_meme(
134 &self,
135 copropietaire: Uuid,
136 convoquee_le: Option<DateTime<Utc>>,
137 moment: DateTime<Utc>,
138 ) -> bool {
139 self.cosignataires.contains(&copropietaire) && self.syndic_defaillant(convoquee_le, moment)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use rust_decimal_macros::dec;
147
148 const TOTAL: Decimal = dec!(1000);
149
150 fn il_y_a(jours: i64) -> DateTime<Utc> {
151 Utc::now() - Duration::days(jours)
152 }
153
154 #[test]
155 fn happy_le_seuil_vaut_un_cinquieme_des_parts() {
156 assert_eq!(seuil_du_cinquieme(TOTAL), dec!(200));
157 }
158
159 #[test]
160 fn happy_une_requete_au_seuil_est_recevable() {
161 let requete =
162 deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(5)).expect("recevable");
163 assert_eq!(requete.quotites_reunies, dec!(200));
164 }
165
166 #[test]
171 fn negative_dix_neuf_petits_porteurs_natteignent_pas_le_seuil() {
172 let cosignataires: Vec<Uuid> = (0..19).map(|_| Uuid::new_v4()).collect();
173 let refus = deposer(cosignataires, dec!(150), TOTAL, il_y_a(5)).expect_err("doit refuser");
174
175 assert_eq!(
176 refus,
177 RequeteIrrecevable::SeuilNonAtteint {
178 quotites_reunies: dec!(150),
179 seuil_requis: dec!(200)
180 }
181 );
182 }
183
184 #[test]
185 fn happy_un_seul_copropietaire_suffisamment_dote_atteint_le_seuil() {
186 assert!(deposer(vec![Uuid::new_v4()], dec!(250), TOTAL, il_y_a(5)).is_ok());
187 }
188
189 #[test]
190 fn negative_une_requete_sans_signataire_est_irrecevable() {
191 assert_eq!(
192 deposer(vec![], dec!(500), TOTAL, il_y_a(5)),
193 Err(RequeteIrrecevable::SansSignataire)
194 );
195 }
196
197 #[test]
200 fn happy_le_delai_court_depuis_la_reception_par_le_syndic() {
201 let recue = il_y_a(10);
202 let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, recue).unwrap();
203 assert_eq!(requete.echeance_convocation, recue + Duration::days(30));
204 }
205
206 #[test]
207 fn happy_un_syndic_qui_convoque_a_temps_nest_pas_defaillant() {
208 let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(40)).unwrap();
209 assert!(!requete.syndic_defaillant(Some(il_y_a(20)), Utc::now()));
210 }
211
212 #[test]
213 fn negative_un_syndic_silencieux_devient_defaillant_passe_trente_jours() {
214 let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(40)).unwrap();
215 assert!(requete.syndic_defaillant(None, Utc::now()));
216 }
217
218 #[test]
219 fn happy_avant_lecheance_le_silence_nest_pas_une_defaillance() {
220 let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(10)).unwrap();
221 assert!(!requete.syndic_defaillant(None, Utc::now()));
222 }
223
224 #[test]
226 fn happy_le_cosignataire_recupere_le_pouvoir_de_convoquer() {
227 let signataire = Uuid::new_v4();
228 let requete = deposer(vec![signataire], dec!(200), TOTAL, il_y_a(40)).unwrap();
229
230 assert!(requete.peut_convoquer_lui_meme(signataire, None, Utc::now()));
231 }
232
233 #[test]
239 fn security_un_non_signataire_ne_recupere_pas_ce_pouvoir() {
240 let signataire = Uuid::new_v4();
241 let passant = Uuid::new_v4();
242 let requete = deposer(vec![signataire], dec!(200), TOTAL, il_y_a(40)).unwrap();
243
244 assert!(!requete.peut_convoquer_lui_meme(passant, None, Utc::now()));
245 }
246
247 #[test]
249 fn edge_le_cosignataire_ne_peut_pas_devancer_lecheance() {
250 let signataire = Uuid::new_v4();
251 let requete = deposer(vec![signataire], dec!(200), TOTAL, il_y_a(10)).unwrap();
252
253 assert!(!requete.peut_convoquer_lui_meme(signataire, None, Utc::now()));
254 }
255
256 #[test]
261 fn edge_une_convocation_tardive_reste_une_defaillance() {
262 let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(40)).unwrap();
263 assert!(requete.syndic_defaillant(Some(il_y_a(3)), Utc::now()));
264 }
265}