Skip to main content

koprogo_api/domain/copropriete/
conflit_dinterets.rs

1//! Le conflit d'intérêts à l'assemblée générale.
2//!
3//! Art. 3.87 § 9 :
4//!
5//! > « Aucune personne **mandatée ou employée** par l'association des
6//! > copropriétaires, ou **prestant pour elle des services dans le cadre de
7//! > tout autre contrat**, ne peut participer personnellement ou par
8//! > procuration aux délibérations et aux votes **relatifs à la mission qui
9//! > lui a été confiée**. »
10//!
11//! La règle est étroite et il faut la lire comme telle : elle n'écarte pas le
12//! prestataire de toute l'assemblée, seulement des points qui **le
13//! concernent**. Un entrepreneur copropriétaire vote normalement sur le
14//! budget ; il ne vote pas sur l'attribution du marché qu'il brigue, ni sur la
15//! réception de ses propres travaux.
16//!
17//! Elle vise aussi la procuration : donner son bulletin à un tiers ne
18//! contourne rien, puisque c'est le mandant qui est écarté autant que le
19//! mandataire. D'où l'usage de `effective_voter_id` **et** de `owner_id`.
20//!
21//! Le syndic en relève au premier chef — il est mandaté par l'association
22//! (Art. 3.89) — et l'Art. 3.89 § 9 y ajoute qu'il ne peut être ni membre du
23//! conseil de copropriété ni commissaire aux comptes dans la même ACP.
24//!
25//! Voir issue #743.
26
27use super::vote::Vote;
28use uuid::Uuid;
29
30/// Un vote exprimé par une personne intéressée à la décision.
31#[derive(Debug, Clone, PartialEq)]
32pub struct ConflitDinterets {
33    /// La personne dont la mission fait l'objet de la délibération.
34    pub interesse: Uuid,
35    /// Le nombre de bulletins qui lui reviennent, en son nom ou par mandat.
36    pub bulletins: usize,
37    /// A-t-il voté par l'intermédiaire d'un mandataire ?
38    pub par_procuration: bool,
39}
40
41impl std::fmt::Display for ConflitDinterets {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        let voie = if self.par_procuration {
44            " (dont par procuration, ce qui ne contourne rien)"
45        } else {
46            ""
47        };
48        write!(
49            f,
50            "Art. 3.87 § 9 : {} est le prestataire de la mission délibérée et a pris part \
51             au vote pour {} bulletin(s){voie}. La délibération est à reprendre sans lui.",
52            self.interesse, self.bulletins
53        )
54    }
55}
56
57/// Vérifie qu'aucun intéressé n'a pris part au vote sur sa propre mission.
58///
59/// `prestataire_de_la_mission` est l'identifiant du copropriétaire, du syndic
60/// ou du prestataire dont la délibération traite la mission. `None` signifie
61/// que la résolution ne porte sur la mission de personne — le cas courant, et
62/// alors il n'y a rien à vérifier.
63///
64/// La vérification couvre les deux voies que la loi nomme :
65///
66/// - **personnellement** : l'intéressé figure comme `owner_id` d'un bulletin ;
67/// - **par procuration** : il figure comme mandataire d'un autre, ou son
68///   propre bulletin a été porté par un tiers. Les deux sont écartés, sinon la
69///   règle se contournerait par un simple échange de pouvoirs.
70pub fn verifier_conflit_dinterets(
71    votes: &[Vote],
72    prestataire_de_la_mission: Option<Uuid>,
73) -> Result<(), ConflitDinterets> {
74    let Some(interesse) = prestataire_de_la_mission else {
75        return Ok(());
76    };
77
78    let bulletins: Vec<&Vote> = votes
79        .iter()
80        .filter(|v| v.owner_id == interesse || v.effective_voter_id() == interesse)
81        .collect();
82
83    if bulletins.is_empty() {
84        return Ok(());
85    }
86
87    Err(ConflitDinterets {
88        interesse,
89        bulletins: bulletins.len(),
90        par_procuration: bulletins.iter().any(|v| v.is_proxy_vote()),
91    })
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::domain::copropriete::vote::VoteChoice;
98    use rust_decimal::Decimal;
99    use rust_decimal_macros::dec;
100
101    fn vote(proprietaire: Uuid, voix: Decimal, mandataire: Option<Uuid>) -> Vote {
102        Vote::new(
103            Uuid::new_v4(),
104            proprietaire,
105            Uuid::new_v4(),
106            VoteChoice::Pour,
107            voix,
108            mandataire,
109        )
110        .expect("vote valide")
111    }
112
113    #[test]
114    fn happy_une_resolution_sans_prestataire_ne_declenche_rien() {
115        let votes = vec![
116            vote(Uuid::new_v4(), dec!(300), None),
117            vote(Uuid::new_v4(), dec!(300), None),
118        ];
119        assert!(verifier_conflit_dinterets(&votes, None).is_ok());
120    }
121
122    #[test]
123    fn happy_le_prestataire_absent_du_vote_ne_pose_pas_de_probleme() {
124        let entrepreneur = Uuid::new_v4();
125        let votes = vec![
126            vote(Uuid::new_v4(), dec!(300), None),
127            vote(Uuid::new_v4(), dec!(300), None),
128        ];
129        assert!(verifier_conflit_dinterets(&votes, Some(entrepreneur)).is_ok());
130    }
131
132    #[test]
133    fn negative_le_prestataire_ne_vote_pas_sur_sa_propre_mission() {
134        let entrepreneur = Uuid::new_v4();
135        let votes = vec![
136            vote(entrepreneur, dec!(150), None),
137            vote(Uuid::new_v4(), dec!(300), None),
138        ];
139
140        let conflit =
141            verifier_conflit_dinterets(&votes, Some(entrepreneur)).expect_err("doit refuser");
142        assert_eq!(conflit.interesse, entrepreneur);
143        assert_eq!(conflit.bulletins, 1);
144        assert!(!conflit.par_procuration);
145    }
146
147    #[test]
148    fn security_donner_procuration_ne_contourne_pas_la_regle() {
149        // L'entrepreneur confie son bulletin à un tiers. La loi écarte le
150        // vote « personnellement OU par procuration » : c'est bien la voix de
151        // l'intéressé qui est écartée, pas seulement sa présence.
152        let entrepreneur = Uuid::new_v4();
153        let complaisant = Uuid::new_v4();
154        let votes = vec![
155            vote(entrepreneur, dec!(150), Some(complaisant)),
156            vote(Uuid::new_v4(), dec!(300), None),
157        ];
158
159        let conflit =
160            verifier_conflit_dinterets(&votes, Some(entrepreneur)).expect_err("doit refuser");
161        assert_eq!(conflit.bulletins, 1);
162        assert!(conflit.par_procuration);
163    }
164
165    #[test]
166    fn security_recevoir_procuration_ne_contourne_pas_davantage() {
167        // L'entrepreneur ne vote pas pour son lot mais porte celui d'un
168        // autre : il « participe par procuration » aux termes de l'article.
169        let entrepreneur = Uuid::new_v4();
170        let votes = vec![
171            vote(Uuid::new_v4(), dec!(150), Some(entrepreneur)),
172            vote(Uuid::new_v4(), dec!(300), None),
173        ];
174
175        let conflit =
176            verifier_conflit_dinterets(&votes, Some(entrepreneur)).expect_err("doit refuser");
177        assert_eq!(conflit.bulletins, 1);
178        assert!(conflit.par_procuration);
179    }
180
181    #[test]
182    fn edge_les_deux_voies_cumulees_comptent_chaque_bulletin() {
183        let entrepreneur = Uuid::new_v4();
184        let votes = vec![
185            vote(entrepreneur, dec!(150), None),
186            vote(Uuid::new_v4(), dec!(100), Some(entrepreneur)),
187            vote(Uuid::new_v4(), dec!(300), None),
188        ];
189
190        let conflit =
191            verifier_conflit_dinterets(&votes, Some(entrepreneur)).expect_err("doit refuser");
192        assert_eq!(conflit.bulletins, 2);
193    }
194}