Skip to main content

koprogo_api/domain/copropriete/
comptes_de_lacp.rs

1//! Les comptes bancaires de l'ACP.
2//!
3//! Art. 3.86 § 3, alinéa 6 :
4//!
5//! > « Ces fonds doivent être placés sur divers comptes, dont **obligatoirement
6//! > un compte distinct pour le fonds de roulement et un compte distinct pour
7//! > le fonds de réserve** ; tous ces comptes doivent être **ouverts au nom de
8//! > l'association des copropriétaires**. »
9//!
10//! Les deux fonds étaient modélisés comme des soldes (ADR-0012). Ce qui
11//! manquait, ce sont les **comptes** eux-mêmes : leur caractère distinct et
12//! leur titulaire.
13//!
14//! C'est exactement la protection que l'article vise, et elle est double :
15//!
16//! 1. **des comptes distincts** empêchent de puiser dans le fonds de réserve
17//!    pour payer les charges courantes. Un compte unique rend l'arbitrage
18//!    invisible — il n'y a rien à franchir ;
19//! 2. **au nom de l'ACP** empêche le syndic de mélanger les fonds d'une
20//!    copropriété avec les siens ou avec ceux d'une autre. C'est le pendant
21//!    bancaire du principe que l'ADR-0045 pose côté données : le patrimoine
22//!    appartient à l'ACP, le syndic ne fait que l'administrer
23//!    (Art. 3.89 § 5, 3°).
24//!
25//! L'article dit « divers comptes, **dont** obligatoirement » : il en impose
26//! deux au minimum, il n'en interdit pas d'autres. Une ACP peut avoir un
27//! compte de travaux en plus, et ce n'est pas une irrégularité.
28//!
29//! Voir issue #756 et ADR-0012.
30
31use uuid::Uuid;
32
33/// L'affectation d'un compte bancaire de l'ACP.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum AffectationDuCompte {
36    /// Fonds de roulement : avances pour les dépenses périodiques.
37    FondsDeRoulement,
38    /// Fonds de réserve : apports pour les dépenses non périodiques.
39    FondsDeReserve,
40    /// Tout autre compte — l'article les autorise (« divers comptes »).
41    Autre,
42}
43
44/// Un compte bancaire, tel que la conformité le regarde.
45#[derive(Debug, Clone, PartialEq)]
46pub struct CompteBancaire {
47    pub id: Uuid,
48    pub iban: String,
49    pub affectation: AffectationDuCompte,
50    /// Le titulaire déclaré du compte.
51    ///
52    /// C'est ce champ qui porte l'exigence « ouverts au nom de l'association
53    /// des copropriétaires ».
54    pub titulaire_acp_id: Option<Uuid>,
55}
56
57impl CompteBancaire {
58    /// Le compte est-il bien ouvert au nom de cette ACP ?
59    pub fn au_nom_de(&self, acp_id: Uuid) -> bool {
60        self.titulaire_acp_id == Some(acp_id)
61    }
62}
63
64/// Ce qui manque à une ACP pour être en règle.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ManquementBancaire {
67    /// Aucun compte affecté au fonds de roulement.
68    FondsDeRoulementSansCompte,
69    /// Aucun compte affecté au fonds de réserve.
70    FondsDeReserveSansCompte,
71    /// Un seul compte sert aux deux fonds.
72    ///
73    /// Le manquement le plus fréquent, et le plus grave des trois : il n'y a
74    /// alors rien à franchir pour puiser dans la réserve.
75    FondsConfondusSurUnSeulCompte { iban: String },
76    /// Un compte n'est pas ouvert au nom de l'ACP.
77    CompteHorsDuNomDeLacp {
78        iban: String,
79        affectation: AffectationDuCompte,
80    },
81}
82
83impl std::fmt::Display for ManquementBancaire {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            Self::FondsDeRoulementSansCompte => write!(
87                f,
88                "Art. 3.86 § 3 : aucun compte distinct pour le fonds de roulement."
89            ),
90            Self::FondsDeReserveSansCompte => write!(
91                f,
92                "Art. 3.86 § 3 : aucun compte distinct pour le fonds de réserve."
93            ),
94            Self::FondsConfondusSurUnSeulCompte { iban } => write!(
95                f,
96                "Art. 3.86 § 3 : le fonds de roulement et le fonds de réserve partagent le \
97                 compte {iban}. Les comptes doivent être distincts — sinon rien n'empêche \
98                 de puiser dans la réserve pour payer les charges courantes."
99            ),
100            Self::CompteHorsDuNomDeLacp { iban, affectation } => write!(
101                f,
102                "Art. 3.86 § 3 : le compte {iban} ({affectation:?}) n'est pas ouvert au nom \
103                 de l'association des copropriétaires."
104            ),
105        }
106    }
107}
108
109/// Vérifie les deux exigences de l'Art. 3.86 § 3, alinéa 6.
110///
111/// Renvoie **tous** les manquements, pas seulement le premier : un syndic qui
112/// doit régulariser a besoin de la liste complète, pas d'un défaut à la fois.
113pub fn verifier(comptes: &[CompteBancaire], acp_id: Uuid) -> Vec<ManquementBancaire> {
114    let mut manquements = Vec::new();
115
116    let roulement: Vec<&CompteBancaire> = comptes
117        .iter()
118        .filter(|c| c.affectation == AffectationDuCompte::FondsDeRoulement)
119        .collect();
120    let reserve: Vec<&CompteBancaire> = comptes
121        .iter()
122        .filter(|c| c.affectation == AffectationDuCompte::FondsDeReserve)
123        .collect();
124
125    if roulement.is_empty() {
126        manquements.push(ManquementBancaire::FondsDeRoulementSansCompte);
127    }
128    if reserve.is_empty() {
129        manquements.push(ManquementBancaire::FondsDeReserveSansCompte);
130    }
131
132    // Deux affectations sur un même IBAN : les fonds sont confondus.
133    for compte_roulement in &roulement {
134        if reserve.iter().any(|r| r.iban == compte_roulement.iban) {
135            manquements.push(ManquementBancaire::FondsConfondusSurUnSeulCompte {
136                iban: compte_roulement.iban.clone(),
137            });
138        }
139    }
140
141    for compte in comptes {
142        if !compte.au_nom_de(acp_id) {
143            manquements.push(ManquementBancaire::CompteHorsDuNomDeLacp {
144                iban: compte.iban.clone(),
145                affectation: compte.affectation,
146            });
147        }
148    }
149
150    manquements
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    fn compte(iban: &str, affectation: AffectationDuCompte, acp: Option<Uuid>) -> CompteBancaire {
158        CompteBancaire {
159            id: Uuid::new_v4(),
160            iban: iban.to_string(),
161            affectation,
162            titulaire_acp_id: acp,
163        }
164    }
165
166    #[test]
167    fn happy_deux_comptes_distincts_au_nom_de_lacp_sont_en_regle() {
168        let acp = Uuid::new_v4();
169        let comptes = vec![
170            compte(
171                "BE68 5390 0754 7034",
172                AffectationDuCompte::FondsDeRoulement,
173                Some(acp),
174            ),
175            compte(
176                "BE71 0961 2345 6769",
177                AffectationDuCompte::FondsDeReserve,
178                Some(acp),
179            ),
180        ];
181        assert!(verifier(&comptes, acp).is_empty());
182    }
183
184    /// L'article dit « divers comptes, DONT obligatoirement » : deux au
185    /// minimum, pas au maximum.
186    #[test]
187    fn happy_un_troisieme_compte_nest_pas_une_irregularite() {
188        let acp = Uuid::new_v4();
189        let comptes = vec![
190            compte(
191                "BE68 5390 0754 7034",
192                AffectationDuCompte::FondsDeRoulement,
193                Some(acp),
194            ),
195            compte(
196                "BE71 0961 2345 6769",
197                AffectationDuCompte::FondsDeReserve,
198                Some(acp),
199            ),
200            compte("BE62 5100 0754 7061", AffectationDuCompte::Autre, Some(acp)),
201        ];
202        assert!(verifier(&comptes, acp).is_empty());
203    }
204
205    /// @security — le manquement le plus fréquent et le plus grave.
206    ///
207    /// Un compte unique rend l'arbitrage invisible : il n'y a rien à franchir
208    /// pour puiser dans la réserve.
209    #[test]
210    fn security_un_compte_unique_pour_les_deux_fonds_est_signale() {
211        let acp = Uuid::new_v4();
212        let comptes = vec![
213            compte(
214                "BE68 5390 0754 7034",
215                AffectationDuCompte::FondsDeRoulement,
216                Some(acp),
217            ),
218            compte(
219                "BE68 5390 0754 7034",
220                AffectationDuCompte::FondsDeReserve,
221                Some(acp),
222            ),
223        ];
224        let manquements = verifier(&comptes, acp);
225        assert_eq!(
226            manquements,
227            vec![ManquementBancaire::FondsConfondusSurUnSeulCompte {
228                iban: "BE68 5390 0754 7034".to_string()
229            }]
230        );
231    }
232
233    /// @security — le pendant bancaire de l'ADR-0045.
234    ///
235    /// Un compte au nom du cabinet mélange les fonds d'une copropriété avec
236    /// ceux d'une autre, ou avec les siens.
237    #[test]
238    fn security_un_compte_au_nom_du_syndic_est_signale() {
239        let acp = Uuid::new_v4();
240        let comptes = vec![
241            compte(
242                "BE68 5390 0754 7034",
243                AffectationDuCompte::FondsDeRoulement,
244                None,
245            ),
246            compte(
247                "BE71 0961 2345 6769",
248                AffectationDuCompte::FondsDeReserve,
249                Some(acp),
250            ),
251        ];
252        let manquements = verifier(&comptes, acp);
253        assert_eq!(manquements.len(), 1);
254        assert!(matches!(
255            manquements[0],
256            ManquementBancaire::CompteHorsDuNomDeLacp { .. }
257        ));
258    }
259
260    /// @security — un compte au nom d'une AUTRE ACP est tout aussi refusé.
261    #[test]
262    fn security_un_compte_au_nom_dune_autre_acp_est_signale() {
263        let acp = Uuid::new_v4();
264        let voisine = Uuid::new_v4();
265        let comptes = vec![
266            compte(
267                "BE68 5390 0754 7034",
268                AffectationDuCompte::FondsDeRoulement,
269                Some(voisine),
270            ),
271            compte(
272                "BE71 0961 2345 6769",
273                AffectationDuCompte::FondsDeReserve,
274                Some(acp),
275            ),
276        ];
277        assert_eq!(verifier(&comptes, acp).len(), 1);
278    }
279
280    #[test]
281    fn negative_une_acp_sans_compte_cumule_les_deux_manquements() {
282        let manquements = verifier(&[], Uuid::new_v4());
283        assert_eq!(
284            manquements,
285            vec![
286                ManquementBancaire::FondsDeRoulementSansCompte,
287                ManquementBancaire::FondsDeReserveSansCompte
288            ]
289        );
290    }
291
292    /// La liste est complète, pas un défaut à la fois.
293    ///
294    /// Un syndic qui régularise a besoin de tout voir : corriger un point pour
295    /// découvrir le suivant coûte un aller-retour bancaire à chaque fois.
296    #[test]
297    fn happy_tous_les_manquements_remontent_ensemble() {
298        let acp = Uuid::new_v4();
299        let comptes = vec![compte(
300            "BE68 5390 0754 7034",
301            AffectationDuCompte::FondsDeRoulement,
302            None,
303        )];
304        let manquements = verifier(&comptes, acp);
305
306        assert_eq!(
307            manquements.len(),
308            2,
309            "réserve absente ET compte hors du nom"
310        );
311    }
312}