Skip to main content

koprogo_api/domain/copropriete/
envoi_convocation.rs

1//! Le mode d'envoi de la convocation.
2//!
3//! Art. 3.87 § 3, alinéa 3 :
4//!
5//! > « La convocation est effectuée **par envoi recommandé**, à moins que les
6//! > destinataires n'aient accepté, **individuellement, explicitement et par
7//! > écrit**, de recevoir la convocation par un autre moyen de communication.
8//! > Les convocations envoyées à la **dernière adresse connue du syndic à la
9//! > date de l'envoi** sont réputées régulières. »
10//!
11//! Le délai de quinze jours était appliqué. L'accord au courriel ne l'était
12//! pas : le logiciel envoyait par courriel sans que rien ne prouve que le
13//! destinataire l'avait accepté.
14//!
15//! L'enjeu n'est pas formel. Une convocation irrégulière rend l'assemblée
16//! attaquable, et ce sont ses décisions — travaux, budgets, mandats — qui
17//! tombent avec elle. Un copropriétaire mécontent d'un vote n'a qu'à montrer
18//! qu'il n'avait jamais accepté le courriel.
19//!
20//! Les trois adverbes sont cumulatifs et chacun exclut une pratique courante :
21//!
22//! - **individuellement** — une clause du règlement d'ordre intérieur ne vaut
23//!   pas accord. Chaque destinataire donne le sien ;
24//! - **explicitement** — cocher une case d'inscription ne suffit pas si elle ne
25//!   porte pas sur ce point précis ;
26//! - **par écrit** — un accord verbal en assemblée ne laisse rien à produire
27//!   le jour où il est contesté.
28//!
29//! La dernière phrase protège le syndic en sens inverse : il répond de la
30//! dernière adresse **qu'il connaissait à la date de l'envoi**, pas de celle
31//! qu'un copropriétaire lui a communiquée le lendemain. L'adresse doit donc
32//! être figée au moment de l'envoi, pas relue après coup.
33//!
34//! Voir issue #749.
35
36use chrono::{DateTime, Utc};
37use uuid::Uuid;
38
39/// Par quel canal la convocation part.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ModeDenvoi {
42    /// Le mode de droit commun, toujours valable.
43    Recommande,
44    /// Tout autre moyen : courriel, plateforme, remise en main propre.
45    AutreMoyen,
46}
47
48/// L'accord donné par un destinataire à un autre mode d'envoi.
49///
50/// Les trois qualités de l'article sont des champs, pas un booléen : un accord
51/// qui n'en remplit que deux n'en est pas un, et il faut pouvoir dire lequel
52/// manque.
53#[derive(Debug, Clone, PartialEq)]
54pub struct AccordAutreMoyen {
55    pub destinataire: Uuid,
56    /// Donné par ce destinataire lui-même, et non par une clause générale.
57    pub individuel: bool,
58    /// Portant explicitement sur le mode d'envoi des convocations.
59    pub explicite: bool,
60    /// Consigné par écrit, et donc produisible.
61    pub ecrit: bool,
62    pub donne_le: DateTime<Utc>,
63}
64
65impl AccordAutreMoyen {
66    /// L'accord remplit-il les trois conditions cumulatives ?
67    pub fn est_valable(&self) -> bool {
68        self.individuel && self.explicite && self.ecrit
69    }
70
71    /// Ce qui lui manque, nommé.
72    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/// Pourquoi un envoi est irrégulier.
88#[derive(Debug, Clone, PartialEq)]
89pub enum EnvoiIrregulier {
90    /// Envoyé autrement que par recommandé, sans aucun accord.
91    SansAccord { destinataire: Uuid },
92    /// Un accord existe mais ne remplit pas les trois conditions.
93    AccordIncomplet {
94        destinataire: Uuid,
95        manques: Vec<&'static str>,
96    },
97    /// L'accord est postérieur à l'envoi.
98    ///
99    /// Il ne régularise rien : au moment de l'envoi, le destinataire n'avait
100    /// pas encore accepté.
101    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
140/// L'envoi est-il régulier au regard de l'Art. 3.87 § 3 ?
141///
142/// Le recommandé l'est toujours, sans condition : c'est le mode de droit
143/// commun.
144pub 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    /// Le cas constaté : on envoyait par courriel sans rien prouver.
209    #[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    /// Une clause du règlement d'ordre intérieur ne vaut pas accord :
219    /// l'article exige qu'il soit **individuel**.
220    #[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    /// Cocher une case d'inscription ne suffit pas si elle ne porte pas sur ce
237    /// point précis : l'accord doit être **explicite**.
238    #[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    /// Un accord verbal en assemblée ne laisse rien à produire le jour où il
252    /// est contesté : il doit être **écrit**.
253    #[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    /// @security — un accord donné après coup ne régularise pas l'envoi.
282    ///
283    /// Au moment où la convocation est partie, le destinataire n'avait pas
284    /// accepté. Le régulariser après reviendrait à valider rétroactivement une
285    /// assemblée déjà tenue.
286    #[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    /// @edge — accord donné le jour même de l'envoi : il vaut.
297    #[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}