Skip to main content

koprogo_api/domain/comptabilite/
arrieres_mutation.rs

1//! Les arriérés retenus lors de la transmission d'un lot.
2//!
3//! Art. 3.95 :
4//!
5//! > « Lors de la passation de l'acte authentique, le notaire instrumentant
6//! > doit **retenir, sur les sommes dues, les arriérés des charges ordinaires
7//! > et extraordinaires en ce compris les frais de récupération judiciaire et
8//! > extrajudiciaire des charges**, dus par le copropriétaire sortant, ainsi
9//! > que **les frais de transmission des informations** requises en vertu de
10//! > l'article 3.94, §§ 1er à 3. »
11//!
12//! L'état daté portait les arriérés, mais pas la **règle de composition** :
13//! quatre postes, et non un seul. Le notaire retient ce que le syndic lui a
14//! chiffré ; si le chiffre est incomplet, l'ACP perd la différence, faute de
15//! pouvoir la réclamer après coup à un vendeur qui a quitté l'immeuble.
16//!
17//! Les postes les plus oubliés sont les frais de récupération — le
18//! recommandé, l'huissier, l'avocat — et les frais de transmission eux-mêmes,
19//! que le vendeur doit et que le syndic avance.
20//!
21//! **La procédure de contestation compte en jours ouvrables**, et l'article le
22//! dit deux fois :
23//!
24//! > « le notaire instrumentant en avise le syndic par envoi recommandé envoyé
25//! > dans les **trois jours ouvrables** qui suivent la passation de l'acte »
26//!
27//! > « A défaut de saisie-arrêt [...] notifiée dans les **vingt jours
28//! > ouvrables** qui suivent la date de l'envoi recommandé [...], le notaire
29//! > peut valablement payer le montant des arriérés au copropriétaire
30//! > sortant. »
31//!
32//! C'est la confirmation directe de l'arbitrage tenu sur l'Art. 3.94 : quand
33//! le législateur veut des jours ouvrables, il l'écrit. Son silence ailleurs
34//! est délibéré, et les délais de l'état daté se comptent donc en jours
35//! calendaires.
36//!
37//! Sur la définition retenue du jour ouvrable, voir [`jours_ouvrables`] : elle
38//! est explicite parce qu'elle n'est pas unanime.
39//!
40//! Voir issue #755.
41
42use chrono::{Datelike, Duration, NaiveDate, Weekday};
43use rust_decimal::Decimal;
44
45/// Ce que le notaire doit retenir, poste par poste.
46///
47/// Les quatre postes sont séparés parce que trois d'entre eux sont
48/// régulièrement oubliés, et qu'un total opaque ne permet ni de le vérifier ni
49/// de le contester.
50#[derive(Debug, Clone, PartialEq, Default)]
51pub struct ArrieresARetenir {
52    /// Arriérés de charges **ordinaires**.
53    pub charges_ordinaires: Decimal,
54    /// Arriérés de charges **extraordinaires**.
55    pub charges_extraordinaires: Decimal,
56    /// Frais de récupération judiciaire et extrajudiciaire : recommandés,
57    /// huissier, avocat. Poste le plus souvent omis.
58    pub frais_de_recuperation: Decimal,
59    /// Frais de transmission des informations de l'Art. 3.94, §§ 1er à 3 —
60    /// ceux que le syndic avance et que le vendeur doit.
61    pub frais_de_transmission: Decimal,
62}
63
64impl ArrieresARetenir {
65    /// Le total à retenir sur le prix.
66    pub fn total(&self) -> Decimal {
67        self.charges_ordinaires
68            + self.charges_extraordinaires
69            + self.frais_de_recuperation
70            + self.frais_de_transmission
71    }
72
73    /// Les postes non nuls, nommés, pour que le décompte remis au notaire soit
74    /// lisible et contestable.
75    pub fn detail(&self) -> Vec<(&'static str, Decimal)> {
76        [
77            ("Charges ordinaires", self.charges_ordinaires),
78            ("Charges extraordinaires", self.charges_extraordinaires),
79            ("Frais de récupération", self.frais_de_recuperation),
80            (
81                "Frais de transmission (Art. 3.94)",
82                self.frais_de_transmission,
83            ),
84        ]
85        .into_iter()
86        .filter(|(_, montant)| !montant.is_zero())
87        .collect()
88    }
89}
90
91/// La définition du jour ouvrable retenue ici : **tout jour sauf le dimanche
92/// et les jours fériés légaux**.
93///
94/// Le samedi compte, conformément à la tradition civiliste belge, qui le
95/// distingue du « jour ouvré » du droit du travail. La convention est écrite
96/// ici parce qu'elle n'est **pas unanime** et qu'un délai de vingt jours mal
97/// compté fait perdre à l'ACP le droit de saisir.
98///
99/// Les jours fériés sont passés en paramètre plutôt que codés en dur : ils
100/// varient d'une année à l'autre, et une liste figée dans le code se périmerait
101/// en silence.
102pub fn jours_ouvrables(depart: NaiveDate, nombre: i64, feries: &[NaiveDate]) -> NaiveDate {
103    let mut date = depart;
104    let mut restants = nombre;
105    while restants > 0 {
106        date += Duration::days(1);
107        if date.weekday() != Weekday::Sun && !feries.contains(&date) {
108            restants -= 1;
109        }
110    }
111    date
112}
113
114/// Délai laissé au notaire pour aviser le syndic d'une contestation.
115pub const DELAI_AVIS_CONTESTATION_OUVRABLES: i64 = 3;
116
117/// Délai au-delà duquel, sans saisie-arrêt, le notaire peut payer le vendeur.
118pub const DELAI_SAISIE_ARRET_OUVRABLES: i64 = 20;
119
120/// Date limite pour que le notaire avise le syndic d'une contestation.
121pub fn limite_avis_contestation(passation: NaiveDate, feries: &[NaiveDate]) -> NaiveDate {
122    jours_ouvrables(passation, DELAI_AVIS_CONTESTATION_OUVRABLES, feries)
123}
124
125/// Date à partir de laquelle, faute de saisie-arrêt, le notaire peut payer les
126/// arriérés au copropriétaire sortant.
127///
128/// Le compte part de **l'envoi du recommandé**, pas de la passation de l'acte.
129pub fn liberation_des_fonds(envoi_recommande: NaiveDate, feries: &[NaiveDate]) -> NaiveDate {
130    jours_ouvrables(envoi_recommande, DELAI_SAISIE_ARRET_OUVRABLES, feries)
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use rust_decimal_macros::dec;
137
138    fn le(annee: i32, mois: u32, jour: u32) -> NaiveDate {
139        NaiveDate::from_ymd_opt(annee, mois, jour).expect("date valide")
140    }
141
142    #[test]
143    fn happy_le_total_additionne_les_quatre_postes() {
144        let arrieres = ArrieresARetenir {
145            charges_ordinaires: dec!(1200),
146            charges_extraordinaires: dec!(3500),
147            frais_de_recuperation: dec!(280.50),
148            frais_de_transmission: dec!(125),
149        };
150        assert_eq!(arrieres.total(), dec!(5105.50));
151    }
152
153    /// Le poste le plus souvent oublié, et ce qu'il coûte de l'oublier.
154    ///
155    /// Si le chiffre remis au notaire est incomplet, l'ACP perd la différence :
156    /// elle ne pourra pas la réclamer après coup à un vendeur qui a quitté
157    /// l'immeuble.
158    #[test]
159    fn security_oublier_les_frais_de_recuperation_ampute_la_retenue() {
160        let complet = ArrieresARetenir {
161            charges_ordinaires: dec!(1200),
162            frais_de_recuperation: dec!(280.50),
163            ..Default::default()
164        };
165        let ampute = ArrieresARetenir {
166            charges_ordinaires: dec!(1200),
167            ..Default::default()
168        };
169        assert_eq!(complet.total() - ampute.total(), dec!(280.50));
170    }
171
172    #[test]
173    fn happy_le_detail_ne_montre_que_les_postes_non_nuls() {
174        let arrieres = ArrieresARetenir {
175            charges_ordinaires: dec!(1200),
176            frais_de_transmission: dec!(125),
177            ..Default::default()
178        };
179        let detail = arrieres.detail();
180        assert_eq!(detail.len(), 2);
181        assert_eq!(detail[0].0, "Charges ordinaires");
182        assert_eq!(detail[1].0, "Frais de transmission (Art. 3.94)");
183    }
184
185    #[test]
186    fn negative_un_vendeur_a_jour_ne_doit_rien() {
187        let rien = ArrieresARetenir::default();
188        assert_eq!(rien.total(), Decimal::ZERO);
189        assert!(rien.detail().is_empty());
190    }
191
192    // ── Les jours ouvrables ────────────────────────────────────────
193
194    /// Le samedi compte, le dimanche non.
195    ///
196    /// La convention est écrite parce qu'elle n'est pas unanime : le « jour
197    /// ouvrable » civiliste n'est pas le « jour ouvré » du droit du travail.
198    #[test]
199    fn happy_le_samedi_est_ouvrable_le_dimanche_ne_lest_pas() {
200        // Le 2026-06-04 est un jeudi.
201        let jeudi = le(2026, 6, 4);
202        assert_eq!(jeudi.weekday(), Weekday::Thu);
203
204        // +3 ouvrables : vendredi, samedi, lundi (le dimanche saute).
205        assert_eq!(jours_ouvrables(jeudi, 3, &[]), le(2026, 6, 8));
206    }
207
208    #[test]
209    fn happy_un_ferie_repousse_dun_jour() {
210        let jeudi = le(2026, 6, 4);
211        let vendredi_ferie = le(2026, 6, 5);
212        assert_eq!(
213            jours_ouvrables(jeudi, 3, &[vendredi_ferie]),
214            le(2026, 6, 9),
215            "un jour de plus que sans le férié"
216        );
217    }
218
219    #[test]
220    fn happy_le_notaire_a_trois_jours_ouvrables_pour_aviser() {
221        let passation = le(2026, 6, 4);
222        assert_eq!(limite_avis_contestation(passation, &[]), le(2026, 6, 8));
223    }
224
225    /// Le compte des vingt jours part de **l'envoi du recommandé**, pas de la
226    /// passation de l'acte.
227    #[test]
228    fn happy_les_vingt_jours_partent_de_lenvoi_du_recommande() {
229        let envoi = le(2026, 6, 8);
230        let liberation = liberation_des_fonds(envoi, &[]);
231        assert!(liberation > envoi);
232        // Vingt jours ouvrables représentent plus de trois semaines
233        // calendaires, puisque les dimanches ne comptent pas.
234        assert!((liberation - envoi).num_days() >= 23);
235    }
236
237    /// @security — compter en jours calendaires libérerait les fonds trop tôt.
238    ///
239    /// L'ACP perdrait le droit de saisir sur des arriérés qu'elle est encore
240    /// dans les temps de contester.
241    #[test]
242    fn security_le_calcul_calendaire_libererait_les_fonds_trop_tot() {
243        let envoi = le(2026, 6, 8);
244        let en_ouvrables = liberation_des_fonds(envoi, &[]);
245        let en_calendaires = envoi + Duration::days(20);
246        assert!(
247            en_ouvrables > en_calendaires,
248            "les jours ouvrables allongent le délai, ils ne le raccourcissent pas"
249        );
250    }
251}