Skip to main content

koprogo_api/domain/copropriete/
requete_ag.rs

1//! La convocation d'une assemblée sur requête des copropriétaires.
2//!
3//! Art. 3.87 § 2, alinéas 2 et 3 :
4//!
5//! > « le syndic tient une assemblée générale **sur requête d'un ou de
6//! > plusieurs copropriétaires qui possèdent au moins un cinquième des parts
7//! > dans les parties communes**. Cette requête est adressée au syndic **par
8//! > envoi recommandé** et celui-ci adresse la convocation aux copropriétaires
9//! > **dans les trente jours** de la réception de la requête. »
10//!
11//! > « Si le syndic ne donne pas suite à cette requête, **un des
12//! > copropriétaires qui a cosigné la requête peut convoquer lui-même
13//! > l'assemblée générale**. »
14//!
15//! C'est un contre-pouvoir, et le second alinéa en est la sanction : le syndic
16//! ne peut pas enterrer une requête en la laissant sans réponse, puisque son
17//! silence transfère le pouvoir de convoquer à ceux qui l'ont signée. Sans le
18//! suivi du délai, ce transfert n'a aucun déclencheur — un copropriétaire ne
19//! saurait pas à partir de quand il peut agir.
20//!
21//! Le seuil se compte **en quotités, pas en têtes** : « un cinquième des parts
22//! dans les parties communes ». Dix-neuf copropriétaires détenant ensemble
23//! 150 millièmes n'atteignent pas le seuil ; un seul en détenant 200 l'atteint.
24//! Confondre les deux est l'erreur la plus naturelle, et elle prive de leur
25//! droit ceux que l'article veut protéger.
26//!
27//! Voir issue #741.
28
29use chrono::{DateTime, Duration, Utc};
30use rust_decimal::Decimal;
31use uuid::Uuid;
32
33/// Le délai laissé au syndic pour convoquer.
34pub const DELAI_CONVOCATION_JOURS: i64 = 30;
35
36/// Ce qui empêche une requête d'aboutir.
37#[derive(Debug, Clone, PartialEq)]
38pub enum RequeteIrrecevable {
39    /// Les cosignataires ne réunissent pas le cinquième des parts.
40    SeuilNonAtteint {
41        quotites_reunies: Decimal,
42        seuil_requis: Decimal,
43    },
44    /// Aucun cosignataire.
45    SansSignataire,
46}
47
48impl std::fmt::Display for RequeteIrrecevable {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::SeuilNonAtteint {
52                quotites_reunies,
53                seuil_requis,
54            } => write!(
55                f,
56                "Art. 3.87 § 2 : les cosignataires réunissent {quotites_reunies} quotités, \
57                 le cinquième des parts en exige {seuil_requis}."
58            ),
59            Self::SansSignataire => write!(
60                f,
61                "Art. 3.87 § 2 : une requête d'assemblée doit émaner d'au moins un \
62                 copropriétaire."
63            ),
64        }
65    }
66}
67
68/// Une requête d'assemblée générale, recevable.
69#[derive(Debug, Clone, PartialEq)]
70pub struct RequeteAg {
71    pub cosignataires: Vec<Uuid>,
72    pub quotites_reunies: Decimal,
73    /// Date de réception par le syndic — c'est elle qui fait courir le délai,
74    /// pas la date d'envoi ni celle de signature.
75    pub recue_le: DateTime<Utc>,
76    pub echeance_convocation: DateTime<Utc>,
77}
78
79/// Le seuil : un cinquième des parts dans les parties communes.
80pub fn seuil_du_cinquieme(total_des_lots: Decimal) -> Decimal {
81    total_des_lots / Decimal::from(5)
82}
83
84/// Constitue une requête si le seuil est atteint.
85///
86/// `quotites_reunies` est la somme des quotités des cosignataires, pas leur
87/// nombre : l'article compte en parts.
88pub fn deposer(
89    cosignataires: Vec<Uuid>,
90    quotites_reunies: Decimal,
91    total_des_lots: Decimal,
92    recue_le: DateTime<Utc>,
93) -> Result<RequeteAg, RequeteIrrecevable> {
94    if cosignataires.is_empty() {
95        return Err(RequeteIrrecevable::SansSignataire);
96    }
97    let seuil = seuil_du_cinquieme(total_des_lots);
98    if quotites_reunies < seuil {
99        return Err(RequeteIrrecevable::SeuilNonAtteint {
100            quotites_reunies,
101            seuil_requis: seuil,
102        });
103    }
104    Ok(RequeteAg {
105        cosignataires,
106        quotites_reunies,
107        recue_le,
108        echeance_convocation: recue_le + Duration::days(DELAI_CONVOCATION_JOURS),
109    })
110}
111
112impl RequeteAg {
113    /// Le syndic a-t-il laissé passer le délai ?
114    ///
115    /// `convoquee_le` est la date à laquelle il a adressé la convocation, si
116    /// il l'a fait.
117    pub fn syndic_defaillant(
118        &self,
119        convoquee_le: Option<DateTime<Utc>>,
120        moment: DateTime<Utc>,
121    ) -> bool {
122        match convoquee_le {
123            Some(date) => date > self.echeance_convocation,
124            None => moment > self.echeance_convocation,
125        }
126    }
127
128    /// Un cosignataire peut-il convoquer lui-même ?
129    ///
130    /// C'est la sanction du silence (Art. 3.87 § 2, alinéa 3), et elle ne
131    /// s'ouvre qu'aux **cosignataires** : un copropriétaire qui n'a pas signé
132    /// la requête ne récupère pas ce pouvoir.
133    pub fn peut_convoquer_lui_meme(
134        &self,
135        copropietaire: Uuid,
136        convoquee_le: Option<DateTime<Utc>>,
137        moment: DateTime<Utc>,
138    ) -> bool {
139        self.cosignataires.contains(&copropietaire) && self.syndic_defaillant(convoquee_le, moment)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use rust_decimal_macros::dec;
147
148    const TOTAL: Decimal = dec!(1000);
149
150    fn il_y_a(jours: i64) -> DateTime<Utc> {
151        Utc::now() - Duration::days(jours)
152    }
153
154    #[test]
155    fn happy_le_seuil_vaut_un_cinquieme_des_parts() {
156        assert_eq!(seuil_du_cinquieme(TOTAL), dec!(200));
157    }
158
159    #[test]
160    fn happy_une_requete_au_seuil_est_recevable() {
161        let requete =
162            deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(5)).expect("recevable");
163        assert_eq!(requete.quotites_reunies, dec!(200));
164    }
165
166    /// Le seuil se compte en quotités, pas en têtes.
167    ///
168    /// C'est l'erreur la plus naturelle, et elle prive de leur droit ceux que
169    /// l'article veut protéger.
170    #[test]
171    fn negative_dix_neuf_petits_porteurs_natteignent_pas_le_seuil() {
172        let cosignataires: Vec<Uuid> = (0..19).map(|_| Uuid::new_v4()).collect();
173        let refus = deposer(cosignataires, dec!(150), TOTAL, il_y_a(5)).expect_err("doit refuser");
174
175        assert_eq!(
176            refus,
177            RequeteIrrecevable::SeuilNonAtteint {
178                quotites_reunies: dec!(150),
179                seuil_requis: dec!(200)
180            }
181        );
182    }
183
184    #[test]
185    fn happy_un_seul_copropietaire_suffisamment_dote_atteint_le_seuil() {
186        assert!(deposer(vec![Uuid::new_v4()], dec!(250), TOTAL, il_y_a(5)).is_ok());
187    }
188
189    #[test]
190    fn negative_une_requete_sans_signataire_est_irrecevable() {
191        assert_eq!(
192            deposer(vec![], dec!(500), TOTAL, il_y_a(5)),
193            Err(RequeteIrrecevable::SansSignataire)
194        );
195    }
196
197    // ── Le délai et sa sanction ────────────────────────────────────────
198
199    #[test]
200    fn happy_le_delai_court_depuis_la_reception_par_le_syndic() {
201        let recue = il_y_a(10);
202        let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, recue).unwrap();
203        assert_eq!(requete.echeance_convocation, recue + Duration::days(30));
204    }
205
206    #[test]
207    fn happy_un_syndic_qui_convoque_a_temps_nest_pas_defaillant() {
208        let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(40)).unwrap();
209        assert!(!requete.syndic_defaillant(Some(il_y_a(20)), Utc::now()));
210    }
211
212    #[test]
213    fn negative_un_syndic_silencieux_devient_defaillant_passe_trente_jours() {
214        let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(40)).unwrap();
215        assert!(requete.syndic_defaillant(None, Utc::now()));
216    }
217
218    #[test]
219    fn happy_avant_lecheance_le_silence_nest_pas_une_defaillance() {
220        let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(10)).unwrap();
221        assert!(!requete.syndic_defaillant(None, Utc::now()));
222    }
223
224    /// La sanction du silence : le cosignataire convoque lui-même.
225    #[test]
226    fn happy_le_cosignataire_recupere_le_pouvoir_de_convoquer() {
227        let signataire = Uuid::new_v4();
228        let requete = deposer(vec![signataire], dec!(200), TOTAL, il_y_a(40)).unwrap();
229
230        assert!(requete.peut_convoquer_lui_meme(signataire, None, Utc::now()));
231    }
232
233    /// @security — mais seulement lui.
234    ///
235    /// Un copropriétaire qui n'a pas signé la requête ne récupère pas ce
236    /// pouvoir : l'article le réserve à « un des copropriétaires qui a cosigné
237    /// la requête ».
238    #[test]
239    fn security_un_non_signataire_ne_recupere_pas_ce_pouvoir() {
240        let signataire = Uuid::new_v4();
241        let passant = Uuid::new_v4();
242        let requete = deposer(vec![signataire], dec!(200), TOTAL, il_y_a(40)).unwrap();
243
244        assert!(!requete.peut_convoquer_lui_meme(passant, None, Utc::now()));
245    }
246
247    /// @edge — et pas avant que le délai soit écoulé.
248    #[test]
249    fn edge_le_cosignataire_ne_peut_pas_devancer_lecheance() {
250        let signataire = Uuid::new_v4();
251        let requete = deposer(vec![signataire], dec!(200), TOTAL, il_y_a(10)).unwrap();
252
253        assert!(!requete.peut_convoquer_lui_meme(signataire, None, Utc::now()));
254    }
255
256    /// @edge — une convocation tardive reste une défaillance.
257    ///
258    /// Le syndic qui convoque le trente-cinquième jour n'efface pas son
259    /// retard : le cosignataire pouvait déjà agir au trente-et-unième.
260    #[test]
261    fn edge_une_convocation_tardive_reste_une_defaillance() {
262        let requete = deposer(vec![Uuid::new_v4()], dec!(200), TOTAL, il_y_a(40)).unwrap();
263        assert!(requete.syndic_defaillant(Some(il_y_a(3)), Utc::now()));
264    }
265}