Skip to main content

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}