Skip to main content

koprogo_api/domain/copropriete/
solidarite.rs

1//! La solidarité des titulaires de droits réels pour les charges.
2//!
3//! Art. 3.86 § 3, alinéa 8 :
4//!
5//! > « Lorsque la propriété d'un lot est grevée d'un droit d'usufruit, les
6//! > titulaires des droits réels sont **solidairement tenus** du paiement de
7//! > ces charges. »
8//!
9//! Ce que ça change concrètement : quand un lot est démembré, l'ACP n'a pas à
10//! répartir sa créance entre l'usufruitier et le nu-propriétaire, ni à
11//! poursuivre les deux. Elle réclame **le tout à l'un d'eux**, à son choix, et
12//! c'est ensuite entre eux que le partage se règle — hors du champ de la
13//! copropriété.
14//!
15//! L'intérêt pratique est direct : sans la solidarité, un syndic qui ne
16//! parvient pas à joindre l'usufruitier reste bloqué alors que le
17//! nu-propriétaire est solvable et joignable. Avec elle, il poursuit celui
18//! qu'il peut atteindre.
19//!
20//! La règle ne joue **que** pour l'usufruit. L'indivision ordinaire, elle,
21//! répartit au prorata des quotes-parts : deux indivisaires à moitié doivent
22//! chacun la moitié, et l'ACP ne peut pas réclamer le tout à l'un d'eux.
23//! Confondre les deux ferait réclamer à un indivisaire une somme qu'il ne
24//! doit pas.
25//!
26//! Voir issue #739.
27
28use super::unit_owner::OwnershipType;
29use rust_decimal::Decimal;
30use uuid::Uuid;
31
32/// Un titulaire de droit réel sur un lot, tel que le recouvrement le voit.
33#[derive(Debug, Clone, PartialEq)]
34pub struct Titulaire {
35    pub owner_id: Uuid,
36    pub nature: OwnershipType,
37    /// Quote-part de détention (0 à 1), qui sert à répartir hors solidarité.
38    pub quote_part: Decimal,
39}
40
41/// Ce qu'un titulaire doit à l'ACP pour une charge donnée.
42#[derive(Debug, Clone, PartialEq)]
43pub struct Obligation {
44    pub owner_id: Uuid,
45    /// Somme réclamable à ce titulaire.
46    pub montant: Decimal,
47    /// Peut-on lui réclamer le tout, ou seulement sa part ?
48    ///
49    /// `true` uniquement en cas de démembrement par usufruit
50    /// (Art. 3.86 § 3 al. 8). Les obligations solidaires d'un même lot
51    /// portent chacune le montant **entier** : ce n'est pas une somme à
52    /// additionner, mais un choix offert à l'ACP.
53    pub solidaire: bool,
54}
55
56/// Le lot est-il grevé d'un usufruit ?
57///
58/// Il l'est dès qu'un usufruitier y figure. Le nu-propriétaire seul ne suffit
59/// pas — sans usufruitier en face, il est plein propriétaire de fait pour ce
60/// qui nous occupe.
61fn greve_dusufruit(titulaires: &[Titulaire]) -> bool {
62    titulaires
63        .iter()
64        .any(|t| t.nature == OwnershipType::Usufruct)
65}
66
67/// Répartit une charge entre les titulaires d'un lot.
68///
69/// En cas d'usufruit, chaque titulaire de droit réel doit **le tout** : ce
70/// sont des obligations solidaires, pas des parts. En dehors de ce cas, chacun
71/// doit sa quote-part.
72pub fn repartir_charge(titulaires: &[Titulaire], montant: Decimal) -> Vec<Obligation> {
73    if titulaires.is_empty() {
74        return Vec::new();
75    }
76
77    if greve_dusufruit(titulaires) {
78        return titulaires
79            .iter()
80            .map(|t| Obligation {
81                owner_id: t.owner_id,
82                montant,
83                solidaire: true,
84            })
85            .collect();
86    }
87
88    titulaires
89        .iter()
90        .map(|t| Obligation {
91            owner_id: t.owner_id,
92            montant: (montant * t.quote_part).round_dp(2),
93            solidaire: false,
94        })
95        .collect()
96}
97
98/// Les titulaires que l'ACP peut poursuivre pour la totalité d'une charge.
99///
100/// Vide quand il n'y a pas de solidarité : c'est alors le montant de chacun
101/// qui fait foi, pas la faculté de choisir.
102pub fn poursuivables_pour_le_tout(titulaires: &[Titulaire]) -> Vec<Uuid> {
103    if !greve_dusufruit(titulaires) {
104        return Vec::new();
105    }
106    titulaires.iter().map(|t| t.owner_id).collect()
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use rust_decimal_macros::dec;
113
114    fn titulaire(nature: OwnershipType, quote_part: Decimal) -> Titulaire {
115        Titulaire {
116            owner_id: Uuid::new_v4(),
117            nature,
118            quote_part,
119        }
120    }
121
122    #[test]
123    fn happy_un_plein_proprietaire_doit_tout_sans_solidarite() {
124        let titulaires = vec![titulaire(OwnershipType::FullOwner, dec!(1))];
125        let obligations = repartir_charge(&titulaires, dec!(1200));
126
127        assert_eq!(obligations.len(), 1);
128        assert_eq!(obligations[0].montant, dec!(1200));
129        assert!(
130            !obligations[0].solidaire,
131            "seul, il n'y a personne avec qui être solidaire"
132        );
133    }
134
135    #[test]
136    fn happy_lusufruit_rend_les_deux_titulaires_tenus_du_tout() {
137        let titulaires = vec![
138            titulaire(OwnershipType::Usufruct, dec!(0.4)),
139            titulaire(OwnershipType::BareOwner, dec!(0.6)),
140        ];
141        let obligations = repartir_charge(&titulaires, dec!(1200));
142
143        assert_eq!(obligations.len(), 2);
144        for obligation in &obligations {
145            assert_eq!(
146                obligation.montant,
147                dec!(1200),
148                "chacun doit LE TOUT : les obligations solidaires ne s'additionnent pas"
149            );
150            assert!(obligation.solidaire);
151        }
152    }
153
154    #[test]
155    fn happy_lindivision_ordinaire_repartit_au_prorata() {
156        let titulaires = vec![
157            titulaire(OwnershipType::Indivisaire, dec!(0.5)),
158            titulaire(OwnershipType::Indivisaire, dec!(0.5)),
159        ];
160        let obligations = repartir_charge(&titulaires, dec!(1200));
161
162        for obligation in &obligations {
163            assert_eq!(obligation.montant, dec!(600));
164            assert!(
165                !obligation.solidaire,
166                "l'Art. 3.86 § 3 al. 8 ne vise que l'usufruit : réclamer le tout \
167                 à un indivisaire lui ferait payer ce qu'il ne doit pas"
168            );
169        }
170    }
171
172    #[test]
173    fn edge_un_nu_proprietaire_seul_nest_pas_solidaire() {
174        // Sans usufruitier en face, il est plein propriétaire de fait pour ce
175        // qui nous occupe : il n'y a pas de démembrement effectif.
176        let titulaires = vec![titulaire(OwnershipType::BareOwner, dec!(1))];
177        let obligations = repartir_charge(&titulaires, dec!(1200));
178
179        assert!(!obligations[0].solidaire);
180        assert!(poursuivables_pour_le_tout(&titulaires).is_empty());
181    }
182
183    #[test]
184    fn happy_le_syndic_choisit_qui_poursuivre_en_cas_dusufruit() {
185        // L'intérêt pratique de la règle : si l'usufruitier est injoignable,
186        // le syndic poursuit le nu-propriétaire pour la totalité.
187        let usufruitier = titulaire(OwnershipType::Usufruct, dec!(0.4));
188        let nu_proprietaire = titulaire(OwnershipType::BareOwner, dec!(0.6));
189        let attendus = vec![usufruitier.owner_id, nu_proprietaire.owner_id];
190
191        let poursuivables = poursuivables_pour_le_tout(&[usufruitier, nu_proprietaire]);
192
193        assert_eq!(poursuivables, attendus);
194    }
195
196    #[test]
197    fn negative_aucun_titulaire_ne_produit_aucune_obligation() {
198        assert!(repartir_charge(&[], dec!(1200)).is_empty());
199    }
200
201    #[test]
202    fn edge_larrondi_de_la_repartition_reste_au_centime() {
203        // 1000 / 3 ne tombe pas juste : on veut deux décimales exactes, pas
204        // une dérive flottante (ADR-0007).
205        let tiers = dec!(0.333333);
206        let titulaires = vec![
207            titulaire(OwnershipType::Indivisaire, tiers),
208            titulaire(OwnershipType::Indivisaire, tiers),
209            titulaire(OwnershipType::Indivisaire, tiers),
210        ];
211        let obligations = repartir_charge(&titulaires, dec!(1000));
212
213        for obligation in &obligations {
214            assert_eq!(obligation.montant, dec!(333.33));
215        }
216    }
217}