koprogo_api/domain/copropriete/
consignation_pv.rs1use chrono::{DateTime, Duration, Utc};
34use uuid::Uuid;
35
36pub const DELAI_JOURS: i64 = 30;
38
39#[derive(Debug, Clone, PartialEq)]
41pub struct EtatConsignation {
42 pub tenue_le: DateTime<Utc>,
44 pub echeance: DateTime<Utc>,
46 pub consigne_le: Option<DateTime<Utc>>,
48 pub non_transmis: Vec<Uuid>,
50}
51
52impl EtatConsignation {
53 pub fn consignation_faite(&self) -> bool {
54 self.consigne_le.is_some()
55 }
56
57 pub fn transmission_faite(&self) -> bool {
58 self.non_transmis.is_empty()
59 }
60
61 pub fn en_defaut_le(&self, moment: DateTime<Utc>) -> bool {
66 moment > self.echeance && !(self.consignation_faite() && self.transmission_faite())
67 }
68
69 pub fn manquements(&self, moment: DateTime<Utc>) -> Vec<String> {
71 if !self.en_defaut_le(moment) {
72 return Vec::new();
73 }
74 let mut manques = Vec::new();
75 if !self.consignation_faite() {
76 manques.push(format!(
77 "Art. 3.87 § 12 : procès-verbal non consigné au registre (échéance {})",
78 self.echeance.date_naive()
79 ));
80 }
81 if !self.transmission_faite() {
82 manques.push(format!(
83 "Art. 3.87 § 12 : procès-verbal non transmis à {} destinataire(s) (échéance {})",
84 self.non_transmis.len(),
85 self.echeance.date_naive()
86 ));
87 }
88 manques
89 }
90}
91
92pub fn echeance(tenue_le: DateTime<Utc>) -> DateTime<Utc> {
94 tenue_le + Duration::days(DELAI_JOURS)
95}
96
97pub fn etat(
103 tenue_le: DateTime<Utc>,
104 consigne_le: Option<DateTime<Utc>>,
105 destinataires: &[Uuid],
106 transmis_a: &[Uuid],
107) -> EtatConsignation {
108 EtatConsignation {
109 tenue_le,
110 echeance: echeance(tenue_le),
111 consigne_le,
112 non_transmis: destinataires
113 .iter()
114 .filter(|d| !transmis_a.contains(d))
115 .copied()
116 .collect(),
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn il_y_a(jours: i64) -> DateTime<Utc> {
125 Utc::now() - Duration::days(jours)
126 }
127
128 #[test]
129 fn happy_lecheance_tombe_trente_jours_apres_lassemblee() {
130 let ag = il_y_a(0);
131 assert_eq!(echeance(ag), ag + Duration::days(30));
132 }
133
134 #[test]
135 fn happy_tout_fait_dans_les_temps_ne_signale_rien() {
136 let a = Uuid::new_v4();
137 let b = Uuid::new_v4();
138 let etat = etat(il_y_a(10), Some(il_y_a(8)), &[a, b], &[a, b]);
139
140 assert!(etat.consignation_faite());
141 assert!(etat.transmission_faite());
142 assert!(!etat.en_defaut_le(Utc::now()));
143 assert!(etat.manquements(Utc::now()).is_empty());
144 }
145
146 #[test]
148 fn happy_dans_le_delai_rien_nest_encore_reproche() {
149 let etat = etat(il_y_a(10), None, &[Uuid::new_v4()], &[]);
150 assert!(!etat.en_defaut_le(Utc::now()));
151 }
152
153 #[test]
156 fn negative_consigner_sans_transmettre_reste_un_defaut() {
157 let destinataire = Uuid::new_v4();
158 let etat = etat(il_y_a(40), Some(il_y_a(35)), &[destinataire], &[]);
159
160 assert!(etat.consignation_faite());
161 assert!(!etat.transmission_faite());
162 assert!(etat.en_defaut_le(Utc::now()));
163
164 let manques = etat.manquements(Utc::now());
165 assert_eq!(manques.len(), 1);
166 assert!(manques[0].contains("non transmis"));
167 }
168
169 #[test]
170 fn negative_transmettre_sans_consigner_aussi() {
171 let destinataire = Uuid::new_v4();
172 let etat = etat(il_y_a(40), None, &[destinataire], &[destinataire]);
173
174 let manques = etat.manquements(Utc::now());
175 assert_eq!(manques.len(), 1);
176 assert!(manques[0].contains("non consigné"));
177 }
178
179 #[test]
180 fn negative_ne_rien_faire_cumule_les_deux_manquements() {
181 let etat = etat(il_y_a(40), None, &[Uuid::new_v4()], &[]);
182 assert_eq!(etat.manquements(Utc::now()).len(), 2);
183 }
184
185 #[test]
190 fn security_un_seul_destinataire_oublie_suffit_a_faire_defaut() {
191 let servis = vec![Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4()];
192 let oublie = Uuid::new_v4();
193 let mut tous = servis.clone();
194 tous.push(oublie);
195
196 let etat = etat(il_y_a(40), Some(il_y_a(35)), &tous, &servis);
197
198 assert_eq!(etat.non_transmis, vec![oublie]);
199 assert!(etat.en_defaut_le(Utc::now()));
200 }
201
202 #[test]
204 fn edge_le_jour_de_lecheance_nest_pas_encore_un_defaut() {
205 let ag = il_y_a(30);
206 let etat = etat(ag, None, &[Uuid::new_v4()], &[]);
207 assert!(
208 !etat.en_defaut_le(etat.echeance),
209 "« dans les trente jours » inclut le trentième"
210 );
211 assert!(etat.en_defaut_le(etat.echeance + Duration::seconds(1)));
212 }
213
214 #[test]
218 fn edge_le_delai_court_depuis_lassemblee() {
219 let ag = il_y_a(40);
220 let etat = etat(ag, Some(il_y_a(2)), &[], &[]);
221 assert!(
222 etat.consigne_le.unwrap() > etat.echeance,
223 "consigné trente-huit jours après l'AG : hors délai"
224 );
225 }
226
227 #[test]
232 fn negative_sans_destinataire_seule_la_consignation_compte() {
233 let etat = etat(il_y_a(40), Some(il_y_a(35)), &[], &[]);
234 assert!(etat.transmission_faite());
235 assert!(!etat.en_defaut_le(Utc::now()));
236 }
237}