koprogo_api/domain/copropriete/
evaluation_des_contrats.rs1use uuid::Uuid;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum NatureDuContrat {
37 FournitureReguliere,
40 MarchePonctuel,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct ContratDeLacp {
47 pub id: Uuid,
48 pub objet: String,
49 pub nature: NatureDuContrat,
50 pub fournisseur: String,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub enum EvaluationManquante {
57 RapportAbsent { exercice: i32 },
59 ContratsNonEvalues { objets: Vec<String> },
61}
62
63impl std::fmt::Display for EvaluationManquante {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 Self::RapportAbsent { exercice } => write!(
67 f,
68 "Art. 3.89 § 5, 12° : aucun rapport d'évaluation des contrats de fournitures \
69 régulières soumis à l'AGO de l'exercice {exercice}."
70 ),
71 Self::ContratsNonEvalues { objets } => write!(
72 f,
73 "Art. 3.89 § 5, 12° : {} contrat(s) de fournitures régulières absent(s) du \
74 rapport — {}. Ces contrats se reconduisent tacitement ; sans point annuel, \
75 personne ne les rediscute.",
76 objets.len(),
77 objets.join(", ")
78 ),
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub struct RapportDevaluation {
86 pub exercice: i32,
87 pub ago_id: Uuid,
92 pub contrats_evalues: Vec<Uuid>,
94}
95
96pub fn verifier_exercice(
101 exercice: i32,
102 contrats: &[ContratDeLacp],
103 rapports: &[RapportDevaluation],
104) -> Result<(), EvaluationManquante> {
105 let Some(rapport) = rapports.iter().find(|r| r.exercice == exercice) else {
106 return Err(EvaluationManquante::RapportAbsent { exercice });
107 };
108
109 let oublies: Vec<String> = contrats
110 .iter()
111 .filter(|c| c.nature == NatureDuContrat::FournitureReguliere)
112 .filter(|c| !rapport.contrats_evalues.contains(&c.id))
113 .map(|c| format!("{} ({})", c.objet, c.fournisseur))
114 .collect();
115
116 if oublies.is_empty() {
117 Ok(())
118 } else {
119 Err(EvaluationManquante::ContratsNonEvalues { objets: oublies })
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn contrat(objet: &str, nature: NatureDuContrat) -> ContratDeLacp {
128 ContratDeLacp {
129 id: Uuid::new_v4(),
130 objet: objet.to_string(),
131 nature,
132 fournisseur: "Kone SA".to_string(),
133 }
134 }
135
136 #[test]
137 fn happy_un_rapport_complet_satisfait_lobligation() {
138 let ascenseur = contrat("Entretien ascenseur", NatureDuContrat::FournitureReguliere);
139 let nettoyage = contrat("Nettoyage communs", NatureDuContrat::FournitureReguliere);
140 let rapports = vec![RapportDevaluation {
141 exercice: 2026,
142 ago_id: Uuid::new_v4(),
143 contrats_evalues: vec![ascenseur.id, nettoyage.id],
144 }];
145
146 assert!(verifier_exercice(2026, &[ascenseur, nettoyage], &rapports).is_ok());
147 }
148
149 #[test]
150 fn negative_sans_rapport_lexercice_est_en_defaut() {
151 assert_eq!(
152 verifier_exercice(2026, &[], &[]),
153 Err(EvaluationManquante::RapportAbsent { exercice: 2026 })
154 );
155 }
156
157 #[test]
162 fn security_un_contrat_oublie_est_signale_nominativement() {
163 let ascenseur = contrat("Entretien ascenseur", NatureDuContrat::FournitureReguliere);
164 let chauffage = contrat("Entretien chaudière", NatureDuContrat::FournitureReguliere);
165 let rapports = vec![RapportDevaluation {
166 exercice: 2026,
167 ago_id: Uuid::new_v4(),
168 contrats_evalues: vec![ascenseur.id],
169 }];
170
171 match verifier_exercice(2026, &[ascenseur, chauffage], &rapports) {
172 Err(EvaluationManquante::ContratsNonEvalues { objets }) => {
173 assert_eq!(objets.len(), 1);
174 assert!(
175 objets[0].contains("Entretien chaudière") && objets[0].contains("Kone SA"),
176 "le rapport doit nommer le contrat ET son fournisseur : {objets:?}"
177 );
178 }
179 autre => panic!("attendu un manquement : {autre:?}"),
180 }
181 }
182
183 #[test]
189 fn happy_un_marche_ponctuel_nest_pas_attendu_au_rapport() {
190 let toiture = contrat("Réfection toiture", NatureDuContrat::MarchePonctuel);
191 let rapports = vec![RapportDevaluation {
192 exercice: 2026,
193 ago_id: Uuid::new_v4(),
194 contrats_evalues: vec![],
195 }];
196
197 assert!(verifier_exercice(2026, &[toiture], &rapports).is_ok());
198 }
199
200 #[test]
205 fn security_un_rapport_de_lexercice_precedent_ne_couvre_pas_le_suivant() {
206 let rapports = vec![RapportDevaluation {
207 exercice: 2025,
208 ago_id: Uuid::new_v4(),
209 contrats_evalues: vec![],
210 }];
211
212 assert_eq!(
213 verifier_exercice(2026, &[], &rapports),
214 Err(EvaluationManquante::RapportAbsent { exercice: 2026 })
215 );
216 }
217
218 #[test]
219 fn happy_une_acp_sans_contrat_regulier_na_rien_a_evaluer() {
220 let rapports = vec![RapportDevaluation {
221 exercice: 2026,
222 ago_id: Uuid::new_v4(),
223 contrats_evalues: vec![],
224 }];
225 assert!(verifier_exercice(2026, &[], &rapports).is_ok());
226 }
227}