Skip to main content

koprogo_api/domain/copropriete/
releve_notaire.rs

1//! Le relevé des dettes réclamé par le notaire.
2//!
3//! Art. 3.89 § 5, 5° — le syndic est chargé :
4//!
5//! > « de fournir le **relevé des dettes** visées à l'article 3.94, § 2, dans
6//! > les **trente jours** de la demande qui lui en est faite **par le
7//! > notaire**. »
8//!
9//! Le délai est documenté depuis longtemps et l'état daté porte les arriérés.
10//! Ce qui manquait, c'est le **suivi** : une demande de notaire n'était pas un
11//! objet du domaine, donc le dépassement était invisible.
12//!
13//! Il n'est pas anodin. Ce relevé conditionne une vente : le notaire ne peut
14//! pas passer l'acte sans savoir ce que le vendeur doit à l'association. Un
15//! syndic qui laisse filer bloque une transaction entre tiers et engage sa
16//! responsabilité, souvent sans s'en apercevoir — c'est précisément ce qu'un
17//! suivi rend visible **avant** l'échéance plutôt qu'après la plainte.
18//!
19//! À distinguer des délais de l'Art. 3.94, qui portent sur l'**état daté**
20//! lui-même : quinze jours calendaires pour une demande simple (§ 1er), trente
21//! pour une demande notariale par recommandé (§ 2). Ici, c'est le relevé des
22//! dettes du § 2 qui est visé, et son délai court depuis la demande du
23//! notaire.
24//!
25//! Voir issue #752.
26
27use chrono::{DateTime, Duration, Utc};
28use uuid::Uuid;
29
30/// Le délai de l'Art. 3.89 § 5, 5°, en jours calendaires.
31///
32/// Calendaires, comme tous les délais de ce chapitre : l'Art. 3.31 § 2 dit
33/// « jour ouvrable » quand il le veut, et son silence ailleurs est délibéré.
34pub const DELAI_JOURS: i64 = 30;
35
36/// Une demande de relevé adressée par un notaire.
37#[derive(Debug, Clone, PartialEq)]
38pub struct DemandeDeReleve {
39    pub id: Uuid,
40    pub notaire: String,
41    /// Le lot dont la vente est en cours.
42    pub unit_id: Uuid,
43    /// Date de réception par le syndic — c'est elle qui fait courir le délai.
44    pub recue_le: DateTime<Utc>,
45    pub echeance: DateTime<Utc>,
46    /// Date de fourniture du relevé, si le syndic l'a fourni.
47    pub fournie_le: Option<DateTime<Utc>>,
48}
49
50/// L'état d'une demande, dit du point de vue du syndic.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum EtatDemande {
53    /// Fournie dans les temps.
54    HonoreeATemps,
55    /// Fournie, mais après l'échéance.
56    HonoreeEnRetard,
57    /// Pas encore fournie, délai non écoulé.
58    EnCours,
59    /// Pas fournie, délai écoulé. Une vente est bloquée.
60    EnDefaut,
61}
62
63impl DemandeDeReleve {
64    pub fn nouvelle(notaire: String, unit_id: Uuid, recue_le: DateTime<Utc>) -> Self {
65        Self {
66            id: Uuid::new_v4(),
67            notaire,
68            unit_id,
69            recue_le,
70            echeance: recue_le + Duration::days(DELAI_JOURS),
71            fournie_le: None,
72        }
73    }
74
75    pub fn fournir(&mut self, le: DateTime<Utc>) {
76        self.fournie_le = Some(le);
77    }
78
79    pub fn etat(&self, moment: DateTime<Utc>) -> EtatDemande {
80        match self.fournie_le {
81            Some(fournie) if fournie <= self.echeance => EtatDemande::HonoreeATemps,
82            Some(_) => EtatDemande::HonoreeEnRetard,
83            None if moment <= self.echeance => EtatDemande::EnCours,
84            None => EtatDemande::EnDefaut,
85        }
86    }
87
88    /// Combien de jours restent avant l'échéance ?
89    ///
90    /// Négatif une fois dépassée. Sert à alerter **avant** plutôt qu'à
91    /// constater après.
92    pub fn jours_restants(&self, moment: DateTime<Utc>) -> i64 {
93        (self.echeance - moment).num_days()
94    }
95}
96
97/// Les demandes qui appellent une relance, triées par urgence.
98///
99/// `seuil_alerte_jours` est la marge à partir de laquelle on prévient : à sept
100/// jours, un syndic a encore le temps d'agir ; le jour même, il ne l'a plus.
101pub fn a_relancer(
102    demandes: &[DemandeDeReleve],
103    moment: DateTime<Utc>,
104    seuil_alerte_jours: i64,
105) -> Vec<&DemandeDeReleve> {
106    let mut urgentes: Vec<&DemandeDeReleve> = demandes
107        .iter()
108        .filter(|d| {
109            matches!(d.etat(moment), EtatDemande::EnCours | EtatDemande::EnDefaut)
110                && d.jours_restants(moment) <= seuil_alerte_jours
111        })
112        .collect();
113    urgentes.sort_by_key(|d| d.echeance);
114    urgentes
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    fn il_y_a(jours: i64) -> DateTime<Utc> {
122        Utc::now() - Duration::days(jours)
123    }
124
125    fn demande(recue_il_y_a: i64) -> DemandeDeReleve {
126        DemandeDeReleve::nouvelle(
127            "Me Dupont".to_string(),
128            Uuid::new_v4(),
129            il_y_a(recue_il_y_a),
130        )
131    }
132
133    #[test]
134    fn happy_lecheance_tombe_trente_jours_apres_la_demande() {
135        let d = demande(0);
136        assert_eq!(d.echeance, d.recue_le + Duration::days(30));
137    }
138
139    #[test]
140    fn happy_un_releve_fourni_dans_les_temps_est_honore() {
141        let mut d = demande(20);
142        d.fournir(il_y_a(5));
143        assert_eq!(d.etat(Utc::now()), EtatDemande::HonoreeATemps);
144    }
145
146    #[test]
147    fn happy_avant_lecheance_la_demande_est_simplement_en_cours() {
148        assert_eq!(demande(10).etat(Utc::now()), EtatDemande::EnCours);
149    }
150
151    /// Le cas qui bloque une vente.
152    #[test]
153    fn negative_passe_trente_jours_sans_releve_le_syndic_est_en_defaut() {
154        assert_eq!(demande(40).etat(Utc::now()), EtatDemande::EnDefaut);
155    }
156
157    /// @edge — le trentième jour, le syndic est encore dans son délai.
158    #[test]
159    fn edge_le_jour_de_lecheance_nest_pas_encore_un_defaut() {
160        let d = demande(30);
161        assert_eq!(d.etat(d.echeance), EtatDemande::EnCours);
162        assert_eq!(
163            d.etat(d.echeance + Duration::seconds(1)),
164            EtatDemande::EnDefaut
165        );
166    }
167
168    /// Fournir en retard ne réécrit pas l'histoire.
169    ///
170    /// Le notaire a attendu, la vente a été retardée : l'état le dit, même une
171    /// fois le relevé remis.
172    #[test]
173    fn negative_un_releve_fourni_en_retard_reste_marque_comme_tel() {
174        let mut d = demande(40);
175        d.fournir(il_y_a(2));
176        assert_eq!(d.etat(Utc::now()), EtatDemande::HonoreeEnRetard);
177    }
178
179    #[test]
180    fn happy_le_compte_a_rebours_previent_avant_lecheance() {
181        // Le moment est fixé à partir de la demande elle-même : le calculer
182        // avec `Utc::now()` ferait dériver le résultat de quelques
183        // microsecondes, et `num_days()` tronque — cinq jours moins un
184        // battement de cil valent quatre.
185        let d = demande(25);
186        let vingt_cinq_jours_apres = d.recue_le + Duration::days(25);
187        assert_eq!(d.jours_restants(vingt_cinq_jours_apres), 5);
188    }
189
190    #[test]
191    fn happy_il_devient_negatif_une_fois_lecheance_passee() {
192        assert!(demande(40).jours_restants(Utc::now()) < 0);
193    }
194
195    /// Le point de la relance : alerter avant, pas constater après.
196    #[test]
197    fn happy_les_demandes_urgentes_remontent_triees_par_echeance() {
198        let demandes = vec![demande(28), demande(40), demande(5), demande(26)];
199        let urgentes = a_relancer(&demandes, Utc::now(), 7);
200
201        assert_eq!(
202            urgentes.len(),
203            3,
204            "celle reçue il y a 5 jours n'est pas urgente"
205        );
206        assert!(
207            urgentes[0].echeance <= urgentes[1].echeance,
208            "la plus pressée en premier"
209        );
210        assert!(urgentes[0].echeance <= urgentes[2].echeance);
211    }
212
213    #[test]
214    fn happy_une_demande_deja_honoree_ne_se_relance_pas() {
215        let mut honoree = demande(28);
216        honoree.fournir(il_y_a(1));
217        assert!(a_relancer(&[honoree], Utc::now(), 7).is_empty());
218    }
219}