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}