koprogo_api/domain/copropriete/contrat_lie.rs
1//! L'autorisation préalable des contrats liés au syndic.
2//!
3//! Art. 3.89 § 5, 13° — le syndic est chargé :
4//!
5//! > « de **solliciter l'autorisation préalable de l'assemblée générale** pour
6//! > tout contrat entre l'association des copropriétaires et **le syndic, ses
7//! > préposés, ses proches, parents ou alliés jusqu'au troisième degré
8//! > inclus**, ou ceux de son conjoint jusqu'au même degré ; il en est de même
9//! > des contrats entre l'association des copropriétaires et **une entreprise
10//! > dont les personnes susvisées sont propriétaires ou dans le capital de
11//! > laquelle elles détiennent une participation**. »
12//!
13//! Sans cette règle, un syndic peut faire signer à l'ACP un contrat avec sa
14//! propre société sans qu'aucune trace de vote soit exigée. C'est le conflit
15//! d'intérêts sous sa forme la plus directe — non plus un vote biaisé
16//! (Art. 3.87 § 9) mais un contrat conclu hors de tout vote.
17//!
18//! **L'antériorité fait partie de l'invariant.** « Autorisation préalable » :
19//! une résolution votée après la signature ne régularise rien. C'est ce que
20//! vérifie `autorisation_valable`, et c'est le point qu'une implémentation
21//! naïve manquerait — elle se contenterait de l'existence d'une résolution.
22//!
23//! Le lien reste attaché au contrat après coup, pour l'audit et pour le
24//! successeur : un syndic entrant doit pouvoir voir quels engagements de l'ACP
25//! ont été conclus avec le cabinet précédent.
26//!
27//! Voir issue #745 et [`super::conflit_dinterets`], qui traite l'autre face du
28//! même problème.
29
30use chrono::{DateTime, Utc};
31use uuid::Uuid;
32
33/// La nature du lien entre le cocontractant et le syndic.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum LienAvecLeSyndic {
36 /// Le syndic lui-même.
37 LeSyndic,
38 /// Un préposé du syndic.
39 Prepose,
40 /// Parent ou allié jusqu'au troisième degré, du syndic ou de son conjoint.
41 ///
42 /// Le degré est porté pour l'audit : au-delà du troisième, il n'y a plus
43 /// de lien au sens de l'article.
44 Parente { degre: u8 },
45 /// Une entreprise dont une des personnes ci-dessus est propriétaire ou
46 /// détient une participation, **quel qu'en soit le montant**.
47 ///
48 /// L'article ne fixe aucun seuil : détenir une part suffit. Ne pas en
49 /// inventer un.
50 EntrepriseDetenue,
51 /// Une entreprise dans laquelle une de ces personnes exerce des
52 /// **fonctions de direction ou de contrôle**.
53 ///
54 /// Ne rien posséder ne suffit donc pas à échapper à l'obligation : un
55 /// syndic administrateur d'une société de nettoyage y est soumis.
56 EntrepriseDirigee,
57 /// Une entreprise dont une de ces personnes est **salariée ou préposée**.
58 ///
59 /// Le cas le plus discret des trois, et il est visé par le texte au même
60 /// titre que les deux autres.
61 EntrepriseEmployeuse,
62 /// Une entreprise qui détient, **directement ou indirectement**, une
63 /// participation dans le capital du syndic personne morale.
64 ///
65 /// Le lien en sens inverse, que la fin du 13° vise expressément.
66 /// « Indirectement » couvre les holdings : s'arrêter à la détention
67 /// directe laisserait passer l'interposition d'une société.
68 ActionnaireDuSyndic,
69}
70
71impl LienAvecLeSyndic {
72 /// Ce lien déclenche-t-il l'obligation d'autorisation préalable ?
73 ///
74 /// La parenté ne compte que jusqu'au troisième degré inclus. Au-delà,
75 /// l'article ne s'applique pas — et l'étendre serait inventer une règle.
76 pub fn exige_autorisation(&self) -> bool {
77 match self {
78 Self::LeSyndic
79 | Self::Prepose
80 | Self::EntrepriseDetenue
81 | Self::EntrepriseDirigee
82 | Self::EntrepriseEmployeuse
83 | Self::ActionnaireDuSyndic => true,
84 Self::Parente { degre } => *degre <= 3,
85 }
86 }
87}
88
89impl std::fmt::Display for LienAvecLeSyndic {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::LeSyndic => write!(f, "le syndic lui-même"),
93 Self::Prepose => write!(f, "un préposé du syndic"),
94 Self::Parente { degre } => write!(f, "un parent ou allié au {degre}e degré"),
95 Self::EntrepriseDetenue => {
96 write!(f, "une entreprise détenue par le syndic ou ses proches")
97 }
98 Self::EntrepriseDirigee => write!(
99 f,
100 "une entreprise dirigée ou contrôlée par le syndic ou ses proches"
101 ),
102 Self::EntrepriseEmployeuse => write!(
103 f,
104 "une entreprise dont le syndic ou l'un de ses proches est salarié ou préposé"
105 ),
106 Self::ActionnaireDuSyndic => write!(
107 f,
108 "une entreprise détenant une participation, directe ou indirecte, dans le \
109 capital du syndic"
110 ),
111 }
112 }
113}
114
115/// L'autorisation votée par l'assemblée, si elle existe.
116#[derive(Debug, Clone, PartialEq)]
117pub struct AutorisationAssemblee {
118 pub resolution_id: Uuid,
119 pub votee_le: DateTime<Utc>,
120}
121
122/// Pourquoi un contrat est refusé.
123#[derive(Debug, Clone, PartialEq)]
124pub enum ContratRefuse {
125 /// Aucune résolution n'autorise ce contrat.
126 AutorisationAbsente { lien: LienAvecLeSyndic },
127 /// Une résolution existe, mais elle est postérieure à la signature.
128 AutorisationTardive {
129 lien: LienAvecLeSyndic,
130 signe_le: DateTime<Utc>,
131 votee_le: DateTime<Utc>,
132 },
133}
134
135impl std::fmt::Display for ContratRefuse {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 match self {
138 Self::AutorisationAbsente { lien } => write!(
139 f,
140 "Art. 3.89 § 5, 13° : ce contrat est conclu avec {lien} et exige \
141 l'autorisation préalable de l'assemblée générale."
142 ),
143 Self::AutorisationTardive {
144 lien,
145 signe_le,
146 votee_le,
147 } => write!(
148 f,
149 "Art. 3.89 § 5, 13° : contrat avec {lien} signé le {} et autorisé le {} — \
150 l'autorisation doit être PRÉALABLE, une régularisation après coup n'en \
151 est pas une.",
152 signe_le.date_naive(),
153 votee_le.date_naive()
154 ),
155 }
156 }
157}
158
159/// Un contrat conclu par l'ACP peut-il l'être ?
160///
161/// `lien` est `None` pour un cocontractant ordinaire : l'immense majorité des
162/// contrats d'une ACP, et rien n'est exigé d'eux.
163pub fn autorisation_valable(
164 lien: Option<LienAvecLeSyndic>,
165 signe_le: DateTime<Utc>,
166 autorisation: Option<&AutorisationAssemblee>,
167) -> Result<(), ContratRefuse> {
168 let Some(lien) = lien else {
169 return Ok(());
170 };
171 if !lien.exige_autorisation() {
172 return Ok(());
173 }
174 let Some(autorisation) = autorisation else {
175 return Err(ContratRefuse::AutorisationAbsente { lien });
176 };
177 if autorisation.votee_le > signe_le {
178 return Err(ContratRefuse::AutorisationTardive {
179 lien,
180 signe_le,
181 votee_le: autorisation.votee_le,
182 });
183 }
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use chrono::Duration;
191
192 fn il_y_a(jours: i64) -> DateTime<Utc> {
193 Utc::now() - Duration::days(jours)
194 }
195
196 fn autorisation(votee_il_y_a: i64) -> AutorisationAssemblee {
197 AutorisationAssemblee {
198 resolution_id: Uuid::new_v4(),
199 votee_le: il_y_a(votee_il_y_a),
200 }
201 }
202
203 #[test]
204 fn happy_un_contrat_ordinaire_nexige_rien() {
205 assert!(autorisation_valable(None, il_y_a(10), None).is_ok());
206 }
207
208 #[test]
209 fn happy_un_contrat_lie_autorise_avant_signature_passe() {
210 assert!(autorisation_valable(
211 Some(LienAvecLeSyndic::EntrepriseDetenue),
212 il_y_a(10),
213 Some(&autorisation(30)),
214 )
215 .is_ok());
216 }
217
218 /// Le cas que la règle vise : le syndic fait signer à l'ACP un contrat
219 /// avec sa propre société.
220 #[test]
221 fn security_un_contrat_avec_la_societe_du_syndic_sans_vote_est_refuse() {
222 let refus =
223 autorisation_valable(Some(LienAvecLeSyndic::EntrepriseDetenue), il_y_a(10), None)
224 .expect_err("doit refuser");
225
226 assert_eq!(
227 refus,
228 ContratRefuse::AutorisationAbsente {
229 lien: LienAvecLeSyndic::EntrepriseDetenue
230 }
231 );
232 }
233
234 /// **L'antériorité fait partie de l'invariant.**
235 ///
236 /// Une implémentation naïve se contenterait de l'existence d'une
237 /// résolution. « Autorisation préalable » : une régularisation après coup
238 /// n'en est pas une.
239 #[test]
240 fn security_une_autorisation_posterieure_ne_regularise_rien() {
241 let refus = autorisation_valable(
242 Some(LienAvecLeSyndic::LeSyndic),
243 il_y_a(30),
244 Some(&autorisation(10)),
245 )
246 .expect_err("doit refuser");
247
248 assert!(matches!(refus, ContratRefuse::AutorisationTardive { .. }));
249 assert!(
250 refus.to_string().contains("PRÉALABLE"),
251 "le refus doit dire pourquoi : {refus}"
252 );
253 }
254
255 /// @edge — autorisée le jour même de la signature, elle passe.
256 #[test]
257 fn edge_une_autorisation_le_jour_meme_passe() {
258 let moment = il_y_a(10);
259 let autorisation = AutorisationAssemblee {
260 resolution_id: Uuid::new_v4(),
261 votee_le: moment,
262 };
263 assert!(
264 autorisation_valable(Some(LienAvecLeSyndic::Prepose), moment, Some(&autorisation))
265 .is_ok()
266 );
267 }
268
269 // ── Le périmètre du lien ───────────────────────────────────────────
270
271 #[test]
272 fn happy_la_parente_compte_jusquau_troisieme_degre() {
273 for degre in 1..=3 {
274 assert!(
275 LienAvecLeSyndic::Parente { degre }.exige_autorisation(),
276 "le {degre}e degré est inclus"
277 );
278 }
279 }
280
281 /// @edge — au-delà du troisième degré, l'article ne s'applique plus.
282 ///
283 /// L'étendre serait inventer une règle, et bloquer des contrats que la loi
284 /// autorise.
285 #[test]
286 fn edge_au_dela_du_troisieme_degre_larticle_ne_sapplique_plus() {
287 let lien = LienAvecLeSyndic::Parente { degre: 4 };
288 assert!(!lien.exige_autorisation());
289 assert!(autorisation_valable(Some(lien), il_y_a(10), None).is_ok());
290 }
291
292 /// L'article ne fixe aucun seuil de participation : détenir une part
293 /// suffit. Ne pas en inventer un.
294 #[test]
295 fn security_une_participation_meme_minime_declenche_lobligation() {
296 assert!(LienAvecLeSyndic::EntrepriseDetenue.exige_autorisation());
297 }
298
299 /// @security — ne rien posséder ne suffit pas à y échapper.
300 ///
301 /// Le texte vise aussi celui qui **dirige ou contrôle**, et celui qui est
302 /// simplement **salarié ou préposé**. S'arrêter à la détention laisserait
303 /// passer les montages les plus courants : un syndic administrateur d'une
304 /// société de nettoyage, un préposé employé du prestataire.
305 #[test]
306 fn security_diriger_ou_etre_salarie_declenche_aussi_lobligation() {
307 assert!(LienAvecLeSyndic::EntrepriseDirigee.exige_autorisation());
308 assert!(LienAvecLeSyndic::EntrepriseEmployeuse.exige_autorisation());
309 }
310
311 /// @security — et le lien en sens inverse.
312 ///
313 /// La fin du 13° vise l'entreprise qui détient une participation dans le
314 /// capital du syndic personne morale, « directement **ou
315 /// indirectement** » : s'arrêter à la détention directe laisserait passer
316 /// l'interposition d'une holding.
317 #[test]
318 fn security_lactionnaire_du_syndic_declenche_lobligation() {
319 assert!(LienAvecLeSyndic::ActionnaireDuSyndic.exige_autorisation());
320 assert!(autorisation_valable(
321 Some(LienAvecLeSyndic::ActionnaireDuSyndic),
322 il_y_a(10),
323 None
324 )
325 .is_err());
326 }
327
328 #[test]
329 fn happy_le_syndic_et_ses_preposes_sont_vises() {
330 assert!(LienAvecLeSyndic::LeSyndic.exige_autorisation());
331 assert!(LienAvecLeSyndic::Prepose.exige_autorisation());
332 }
333}