koprogo_api/domain/copropriete/
mention_numero_entreprise.rs1#[derive(Debug, Clone, PartialEq)]
30pub struct DocumentProduit {
31 pub nature: &'static str,
33 pub contenu: String,
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub enum MentionNumeroEntreprise {
40 Presente,
42 NumeroNonAttribue,
48 Manquante { documents: Vec<&'static str> },
50}
51
52impl std::fmt::Display for MentionNumeroEntreprise {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Self::Presente => write!(f, "Numéro d'entreprise mentionné sur tous les documents"),
56 Self::NumeroNonAttribue => write!(
57 f,
58 "Numéro d'entreprise non attribué : l'ACP n'a pas encore acquis la \
59 personnalité juridique (Art. 3.86 § 1er)"
60 ),
61 Self::Manquante { documents } => write!(
62 f,
63 "Art. 3.86 § 1er : le numéro d'entreprise manque sur {} — « tous les \
64 documents émanant de l'association des copropriétaires mentionnent le \
65 numéro d'entreprise ».",
66 documents.join(", ")
67 ),
68 }
69 }
70}
71
72pub fn verifier_les_documents(
78 numero_entreprise: Option<&str>,
79 documents: &[DocumentProduit],
80) -> MentionNumeroEntreprise {
81 let Some(numero) = numero_entreprise else {
82 return MentionNumeroEntreprise::NumeroNonAttribue;
83 };
84
85 let normalise = |s: &str| {
86 s.chars()
87 .filter(|c| c.is_alphanumeric())
88 .collect::<String>()
89 };
90 let attendu = normalise(numero);
91
92 let manquants: Vec<&'static str> = documents
93 .iter()
94 .filter(|d| !normalise(&d.contenu).contains(&attendu))
95 .map(|d| d.nature)
96 .collect();
97
98 if manquants.is_empty() {
99 MentionNumeroEntreprise::Presente
100 } else {
101 MentionNumeroEntreprise::Manquante {
102 documents: manquants,
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn document(nature: &'static str, contenu: &str) -> DocumentProduit {
112 DocumentProduit {
113 nature,
114 contenu: contenu.to_string(),
115 }
116 }
117
118 const NUMERO: &str = "BE 0123.456.789";
119
120 #[test]
121 fn happy_un_document_portant_la_mention_passe() {
122 let docs = vec![document(
123 "convocation",
124 "ACP Résidence du Parc — BE 0123.456.789\nConvocation à l'AGO...",
125 )];
126 assert_eq!(
127 verifier_les_documents(Some(NUMERO), &docs),
128 MentionNumeroEntreprise::Presente
129 );
130 }
131
132 #[test]
137 fn happy_la_forme_compacte_est_acceptee() {
138 let docs = vec![document("état daté", "ACP — BE0123456789\nÉtat daté...")];
139 assert_eq!(
140 verifier_les_documents(Some(NUMERO), &docs),
141 MentionNumeroEntreprise::Presente
142 );
143 }
144
145 #[test]
146 fn negative_un_document_sans_mention_est_signale() {
147 let docs = vec![document(
148 "appel de fonds",
149 "Appel de fonds T1 2026\nMontant : 1200 €",
150 )];
151 assert_eq!(
152 verifier_les_documents(Some(NUMERO), &docs),
153 MentionNumeroEntreprise::Manquante {
154 documents: vec!["appel de fonds"]
155 }
156 );
157 }
158
159 #[test]
161 fn security_un_seul_oubli_parmi_cinq_est_releve() {
162 let avec = format!("En-tête — {NUMERO}\nCorps du document");
163 let docs = vec![
164 document("convocation", &avec),
165 document("procès-verbal", &avec),
166 document("appel de fonds", "Appel de fonds T1 2026"),
167 document("état daté", &avec),
168 document("décompte", &avec),
169 ];
170 assert_eq!(
171 verifier_les_documents(Some(NUMERO), &docs),
172 MentionNumeroEntreprise::Manquante {
173 documents: vec!["appel de fonds"]
174 }
175 );
176 }
177
178 #[test]
179 fn negative_plusieurs_oublis_remontent_ensemble() {
180 let docs = vec![
181 document("convocation", "Convocation"),
182 document("relance", "Relance de paiement"),
183 ];
184 match verifier_les_documents(Some(NUMERO), &docs) {
185 MentionNumeroEntreprise::Manquante { documents } => {
186 assert_eq!(documents.len(), 2);
187 }
188 autre => panic!("attendu un manquement : {autre}"),
189 }
190 }
191
192 #[test]
198 fn edge_une_acp_sans_numero_nest_pas_en_manquement() {
199 let docs = vec![document("convocation", "Convocation à l'AGO")];
200 assert_eq!(
201 verifier_les_documents(None, &docs),
202 MentionNumeroEntreprise::NumeroNonAttribue
203 );
204 }
205
206 #[test]
207 fn happy_aucun_document_produit_ne_pose_pas_de_probleme() {
208 assert_eq!(
209 verifier_les_documents(Some(NUMERO), &[]),
210 MentionNumeroEntreprise::Presente
211 );
212 }
213
214 #[test]
219 fn security_le_numero_dune_autre_acp_ne_satisfait_pas_lobligation() {
220 let docs = vec![document(
221 "convocation",
222 "ACP voisine — BE 0987.654.321\nConvocation",
223 )];
224 assert!(matches!(
225 verifier_les_documents(Some(NUMERO), &docs),
226 MentionNumeroEntreprise::Manquante { .. }
227 ));
228 }
229}