koprogo_api/domain/copropriete/
envoi_convocation.rs1use chrono::{DateTime, Utc};
37use uuid::Uuid;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ModeDenvoi {
42 Recommande,
44 AutreMoyen,
46}
47
48#[derive(Debug, Clone, PartialEq)]
54pub struct AccordAutreMoyen {
55 pub destinataire: Uuid,
56 pub individuel: bool,
58 pub explicite: bool,
60 pub ecrit: bool,
62 pub donne_le: DateTime<Utc>,
63}
64
65impl AccordAutreMoyen {
66 pub fn est_valable(&self) -> bool {
68 self.individuel && self.explicite && self.ecrit
69 }
70
71 pub fn qualites_manquantes(&self) -> Vec<&'static str> {
73 let mut manques = Vec::new();
74 if !self.individuel {
75 manques.push("individuel");
76 }
77 if !self.explicite {
78 manques.push("explicite");
79 }
80 if !self.ecrit {
81 manques.push("écrit");
82 }
83 manques
84 }
85}
86
87#[derive(Debug, Clone, PartialEq)]
89pub enum EnvoiIrregulier {
90 SansAccord { destinataire: Uuid },
92 AccordIncomplet {
94 destinataire: Uuid,
95 manques: Vec<&'static str>,
96 },
97 AccordPosterieurALenvoi {
102 destinataire: Uuid,
103 envoye_le: DateTime<Utc>,
104 accord_donne_le: DateTime<Utc>,
105 },
106}
107
108impl std::fmt::Display for EnvoiIrregulier {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 match self {
111 Self::SansAccord { destinataire } => write!(
112 f,
113 "Art. 3.87 § 3 : convocation envoyée à {destinataire} par un autre moyen que \
114 le recommandé, sans accord de sa part."
115 ),
116 Self::AccordIncomplet {
117 destinataire,
118 manques,
119 } => write!(
120 f,
121 "Art. 3.87 § 3 : l'accord de {destinataire} n'est pas {} — les trois \
122 qualités sont cumulatives.",
123 manques.join(", ni ")
124 ),
125 Self::AccordPosterieurALenvoi {
126 destinataire,
127 envoye_le,
128 accord_donne_le,
129 } => write!(
130 f,
131 "Art. 3.87 § 3 : convocation envoyée à {destinataire} le {} alors que son \
132 accord date du {} — au moment de l'envoi, il n'avait pas accepté.",
133 envoye_le.date_naive(),
134 accord_donne_le.date_naive()
135 ),
136 }
137 }
138}
139
140pub fn envoi_regulier(
145 destinataire: Uuid,
146 mode: ModeDenvoi,
147 envoye_le: DateTime<Utc>,
148 accord: Option<&AccordAutreMoyen>,
149) -> Result<(), EnvoiIrregulier> {
150 if mode == ModeDenvoi::Recommande {
151 return Ok(());
152 }
153 let Some(accord) = accord else {
154 return Err(EnvoiIrregulier::SansAccord { destinataire });
155 };
156 if !accord.est_valable() {
157 return Err(EnvoiIrregulier::AccordIncomplet {
158 destinataire,
159 manques: accord.qualites_manquantes(),
160 });
161 }
162 if accord.donne_le > envoye_le {
163 return Err(EnvoiIrregulier::AccordPosterieurALenvoi {
164 destinataire,
165 envoye_le,
166 accord_donne_le: accord.donne_le,
167 });
168 }
169 Ok(())
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use chrono::Duration;
176
177 fn il_y_a(jours: i64) -> DateTime<Utc> {
178 Utc::now() - Duration::days(jours)
179 }
180
181 fn accord_complet(destinataire: Uuid, donne_il_y_a: i64) -> AccordAutreMoyen {
182 AccordAutreMoyen {
183 destinataire,
184 individuel: true,
185 explicite: true,
186 ecrit: true,
187 donne_le: il_y_a(donne_il_y_a),
188 }
189 }
190
191 #[test]
192 fn happy_le_recommande_est_toujours_regulier() {
193 assert!(envoi_regulier(Uuid::new_v4(), ModeDenvoi::Recommande, il_y_a(20), None).is_ok());
194 }
195
196 #[test]
197 fn happy_un_courriel_couvert_par_un_accord_complet_est_regulier() {
198 let d = Uuid::new_v4();
199 assert!(envoi_regulier(
200 d,
201 ModeDenvoi::AutreMoyen,
202 il_y_a(20),
203 Some(&accord_complet(d, 400))
204 )
205 .is_ok());
206 }
207
208 #[test]
210 fn security_un_courriel_sans_accord_rend_la_convocation_irreguliere() {
211 let d = Uuid::new_v4();
212 assert_eq!(
213 envoi_regulier(d, ModeDenvoi::AutreMoyen, il_y_a(20), None),
214 Err(EnvoiIrregulier::SansAccord { destinataire: d })
215 );
216 }
217
218 #[test]
221 fn security_une_clause_generale_du_roi_ne_vaut_pas_accord() {
222 let d = Uuid::new_v4();
223 let clause = AccordAutreMoyen {
224 individuel: false,
225 ..accord_complet(d, 400)
226 };
227 assert_eq!(
228 envoi_regulier(d, ModeDenvoi::AutreMoyen, il_y_a(20), Some(&clause)),
229 Err(EnvoiIrregulier::AccordIncomplet {
230 destinataire: d,
231 manques: vec!["individuel"]
232 })
233 );
234 }
235
236 #[test]
239 fn security_une_case_cochee_a_linscription_ne_suffit_pas() {
240 let d = Uuid::new_v4();
241 let vague = AccordAutreMoyen {
242 explicite: false,
243 ..accord_complet(d, 400)
244 };
245 assert!(matches!(
246 envoi_regulier(d, ModeDenvoi::AutreMoyen, il_y_a(20), Some(&vague)),
247 Err(EnvoiIrregulier::AccordIncomplet { .. })
248 ));
249 }
250
251 #[test]
254 fn security_un_accord_verbal_ne_suffit_pas() {
255 let d = Uuid::new_v4();
256 let verbal = AccordAutreMoyen {
257 ecrit: false,
258 ..accord_complet(d, 400)
259 };
260 assert!(matches!(
261 envoi_regulier(d, ModeDenvoi::AutreMoyen, il_y_a(20), Some(&verbal)),
262 Err(EnvoiIrregulier::AccordIncomplet { .. })
263 ));
264 }
265
266 #[test]
267 fn negative_les_trois_qualites_manquantes_sont_toutes_nommees() {
268 let d = Uuid::new_v4();
269 let rien = AccordAutreMoyen {
270 individuel: false,
271 explicite: false,
272 ecrit: false,
273 ..accord_complet(d, 400)
274 };
275 assert_eq!(
276 rien.qualites_manquantes(),
277 vec!["individuel", "explicite", "écrit"]
278 );
279 }
280
281 #[test]
287 fn security_un_accord_posterieur_ne_regularise_pas_lenvoi() {
288 let d = Uuid::new_v4();
289 let tardif = accord_complet(d, 5);
290 assert!(matches!(
291 envoi_regulier(d, ModeDenvoi::AutreMoyen, il_y_a(20), Some(&tardif)),
292 Err(EnvoiIrregulier::AccordPosterieurALenvoi { .. })
293 ));
294 }
295
296 #[test]
298 fn edge_un_accord_du_jour_meme_couvre_lenvoi() {
299 let d = Uuid::new_v4();
300 let moment = il_y_a(20);
301 let accord = AccordAutreMoyen {
302 donne_le: moment,
303 ..accord_complet(d, 0)
304 };
305 assert!(envoi_regulier(d, ModeDenvoi::AutreMoyen, moment, Some(&accord)).is_ok());
306 }
307}