1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum DecisionStatus {
8 Pending, InProgress, Completed, Overdue, Cancelled, }
14
15impl std::fmt::Display for DecisionStatus {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 DecisionStatus::Pending => write!(f, "pending"),
19 DecisionStatus::InProgress => write!(f, "in_progress"),
20 DecisionStatus::Completed => write!(f, "completed"),
21 DecisionStatus::Overdue => write!(f, "overdue"),
22 DecisionStatus::Cancelled => write!(f, "cancelled"),
23 }
24 }
25}
26
27impl std::str::FromStr for DecisionStatus {
28 type Err = String;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 match s.to_lowercase().as_str() {
32 "pending" => Ok(DecisionStatus::Pending),
33 "in_progress" => Ok(DecisionStatus::InProgress),
34 "completed" => Ok(DecisionStatus::Completed),
35 "overdue" => Ok(DecisionStatus::Overdue),
36 "cancelled" => Ok(DecisionStatus::Cancelled),
37 _ => Err(format!("Invalid decision status: {}", s)),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct BoardDecision {
46 pub id: Uuid,
47 pub building_id: Uuid,
48 pub meeting_id: Uuid, pub subject: String, pub decision_text: String, pub deadline: Option<DateTime<Utc>>, pub status: DecisionStatus,
53 pub completed_at: Option<DateTime<Utc>>,
54 pub notes: Option<String>, pub created_at: DateTime<Utc>,
56 pub updated_at: DateTime<Utc>,
57}
58
59impl BoardDecision {
60 pub fn new(
62 building_id: Uuid,
63 meeting_id: Uuid,
64 subject: String,
65 decision_text: String,
66 deadline: Option<DateTime<Utc>>,
67 ) -> Result<Self, String> {
68 if subject.trim().is_empty() {
70 return Err("Decision subject cannot be empty".to_string());
71 }
72
73 if decision_text.trim().is_empty() {
75 return Err("Decision text cannot be empty".to_string());
76 }
77
78 if let Some(deadline_date) = deadline {
80 if deadline_date <= Utc::now() {
81 return Err("Deadline must be in the future".to_string());
82 }
83 }
84
85 let now = Utc::now();
86 Ok(Self {
87 id: Uuid::new_v4(),
88 building_id,
89 meeting_id,
90 subject,
91 decision_text,
92 deadline,
93 status: DecisionStatus::Pending,
94 completed_at: None,
95 notes: None,
96 created_at: now,
97 updated_at: now,
98 })
99 }
100
101 pub fn is_overdue(&self) -> bool {
103 if self.status == DecisionStatus::Completed || self.status == DecisionStatus::Cancelled {
104 return false;
105 }
106
107 if let Some(deadline) = self.deadline {
108 Utc::now() > deadline
109 } else {
110 false
111 }
112 }
113
114 pub fn update_status(&mut self, new_status: DecisionStatus) -> Result<(), String> {
117 match (&self.status, &new_status) {
119 (DecisionStatus::Completed, _) => {
121 return Err("Cannot change status of a completed decision".to_string());
122 }
123 (DecisionStatus::Cancelled, _) => {
124 return Err("Cannot change status of a cancelled decision".to_string());
125 }
126 (DecisionStatus::Pending, DecisionStatus::InProgress)
128 | (DecisionStatus::Pending, DecisionStatus::Cancelled)
129 | (DecisionStatus::InProgress, DecisionStatus::Completed)
130 | (DecisionStatus::InProgress, DecisionStatus::Cancelled)
131 | (DecisionStatus::Overdue, DecisionStatus::InProgress)
132 | (DecisionStatus::Overdue, DecisionStatus::Completed)
133 | (DecisionStatus::Overdue, DecisionStatus::Cancelled) => {}
134 _ => {
136 return Err(format!(
137 "Invalid status transition from {} to {}",
138 self.status, new_status
139 ));
140 }
141 }
142
143 self.status = new_status.clone();
144 self.updated_at = Utc::now();
145
146 if new_status == DecisionStatus::Completed {
148 self.completed_at = Some(Utc::now());
149 }
150
151 Ok(())
152 }
153
154 pub fn add_notes(&mut self, notes: String) {
156 self.notes = Some(notes);
157 self.updated_at = Utc::now();
158 }
159
160 pub fn check_and_update_overdue_status(&mut self) {
162 if self.is_overdue() && self.status != DecisionStatus::Overdue {
163 self.status = DecisionStatus::Overdue;
164 self.updated_at = Utc::now();
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use chrono::Duration;
173
174 #[test]
175 fn test_create_decision_success() {
176 let building_id = Uuid::new_v4();
178 let meeting_id = Uuid::new_v4();
179 let subject = "Réparation ascenseur".to_string();
180 let text = "Approuver les travaux de réparation de l'ascenseur pour un montant de 15000€"
181 .to_string();
182 let deadline = Some(Utc::now() + Duration::days(60));
183
184 let result = BoardDecision::new(
186 building_id,
187 meeting_id,
188 subject.clone(),
189 text.clone(),
190 deadline,
191 );
192
193 assert!(result.is_ok());
195 let decision = result.unwrap();
196 assert_eq!(decision.building_id, building_id);
197 assert_eq!(decision.meeting_id, meeting_id);
198 assert_eq!(decision.subject, subject);
199 assert_eq!(decision.decision_text, text);
200 assert_eq!(decision.status, DecisionStatus::Pending);
201 assert!(decision.completed_at.is_none());
202 assert!(decision.deadline.is_some());
203 }
204
205 #[test]
206 fn test_create_decision_empty_subject_fails() {
207 let subject = " ".to_string(); let result = BoardDecision::new(
212 Uuid::new_v4(),
213 Uuid::new_v4(),
214 subject,
215 "Some text".to_string(),
216 None,
217 );
218
219 assert!(result.is_err());
221 assert_eq!(result.unwrap_err(), "Decision subject cannot be empty");
222 }
223
224 #[test]
225 fn test_create_decision_empty_text_fails() {
226 let text = "".to_string();
228
229 let result = BoardDecision::new(
231 Uuid::new_v4(),
232 Uuid::new_v4(),
233 "Subject".to_string(),
234 text,
235 None,
236 );
237
238 assert!(result.is_err());
240 assert_eq!(result.unwrap_err(), "Decision text cannot be empty");
241 }
242
243 #[test]
244 fn test_create_decision_past_deadline_fails() {
245 let deadline = Some(Utc::now() - Duration::days(1)); let result = BoardDecision::new(
250 Uuid::new_v4(),
251 Uuid::new_v4(),
252 "Subject".to_string(),
253 "Text".to_string(),
254 deadline,
255 );
256
257 assert!(result.is_err());
259 assert_eq!(result.unwrap_err(), "Deadline must be in the future");
260 }
261
262 #[test]
263 fn test_is_overdue_true() {
264 let deadline = Some(Utc::now() - Duration::days(1)); let decision = BoardDecision {
267 id: Uuid::new_v4(),
268 building_id: Uuid::new_v4(),
269 meeting_id: Uuid::new_v4(),
270 subject: "Test".to_string(),
271 decision_text: "Test text".to_string(),
272 deadline,
273 status: DecisionStatus::Pending,
274 completed_at: None,
275 notes: None,
276 created_at: Utc::now(),
277 updated_at: Utc::now(),
278 };
279
280 assert!(decision.is_overdue());
282 }
283
284 #[test]
285 fn test_is_overdue_false_no_deadline() {
286 let decision = BoardDecision {
288 id: Uuid::new_v4(),
289 building_id: Uuid::new_v4(),
290 meeting_id: Uuid::new_v4(),
291 subject: "Test".to_string(),
292 decision_text: "Test text".to_string(),
293 deadline: None, status: DecisionStatus::Pending,
295 completed_at: None,
296 notes: None,
297 created_at: Utc::now(),
298 updated_at: Utc::now(),
299 };
300
301 assert!(!decision.is_overdue());
303 }
304
305 #[test]
306 fn test_is_overdue_false_completed() {
307 let deadline = Some(Utc::now() - Duration::days(1)); let decision = BoardDecision {
310 id: Uuid::new_v4(),
311 building_id: Uuid::new_v4(),
312 meeting_id: Uuid::new_v4(),
313 subject: "Test".to_string(),
314 decision_text: "Test text".to_string(),
315 deadline,
316 status: DecisionStatus::Completed,
317 completed_at: Some(Utc::now() - Duration::hours(2)),
318 notes: None,
319 created_at: Utc::now(),
320 updated_at: Utc::now(),
321 };
322
323 assert!(!decision.is_overdue()); }
326
327 #[test]
328 fn test_update_status_valid_transitions() {
329 let mut decision = BoardDecision::new(
331 Uuid::new_v4(),
332 Uuid::new_v4(),
333 "Test".to_string(),
334 "Text".to_string(),
335 Some(Utc::now() + Duration::days(30)),
336 )
337 .unwrap();
338
339 assert!(decision.update_status(DecisionStatus::InProgress).is_ok());
341 assert_eq!(decision.status, DecisionStatus::InProgress);
342
343 assert!(decision.update_status(DecisionStatus::Completed).is_ok());
345 assert_eq!(decision.status, DecisionStatus::Completed);
346 assert!(decision.completed_at.is_some());
347 }
348
349 #[test]
350 fn test_update_status_cannot_modify_completed() {
351 let mut decision = BoardDecision::new(
353 Uuid::new_v4(),
354 Uuid::new_v4(),
355 "Test".to_string(),
356 "Text".to_string(),
357 None,
358 )
359 .unwrap();
360 decision.update_status(DecisionStatus::InProgress).unwrap();
361 decision.update_status(DecisionStatus::Completed).unwrap();
362
363 let result = decision.update_status(DecisionStatus::Pending);
365
366 assert!(result.is_err());
368 assert_eq!(
369 result.unwrap_err(),
370 "Cannot change status of a completed decision"
371 );
372 }
373
374 #[test]
375 fn test_update_status_invalid_transition() {
376 let mut decision = BoardDecision::new(
378 Uuid::new_v4(),
379 Uuid::new_v4(),
380 "Test".to_string(),
381 "Text".to_string(),
382 None,
383 )
384 .unwrap();
385
386 let result = decision.update_status(DecisionStatus::Completed);
388
389 assert!(result.is_err());
391 assert!(result.unwrap_err().contains("Invalid status transition"));
392 }
393
394 #[test]
395 fn test_add_notes() {
396 let mut decision = BoardDecision::new(
398 Uuid::new_v4(),
399 Uuid::new_v4(),
400 "Test".to_string(),
401 "Text".to_string(),
402 None,
403 )
404 .unwrap();
405
406 decision.add_notes("Le syndic a confirmé le début des travaux".to_string());
408
409 assert!(decision.notes.is_some());
411 assert_eq!(
412 decision.notes.unwrap(),
413 "Le syndic a confirmé le début des travaux"
414 );
415 }
416
417 #[test]
418 fn test_check_and_update_overdue_status() {
419 let deadline = Some(Utc::now() - Duration::days(1)); let mut decision = BoardDecision {
422 id: Uuid::new_v4(),
423 building_id: Uuid::new_v4(),
424 meeting_id: Uuid::new_v4(),
425 subject: "Test".to_string(),
426 decision_text: "Test text".to_string(),
427 deadline,
428 status: DecisionStatus::Pending,
429 completed_at: None,
430 notes: None,
431 created_at: Utc::now(),
432 updated_at: Utc::now(),
433 };
434
435 decision.check_and_update_overdue_status();
437
438 assert_eq!(decision.status, DecisionStatus::Overdue);
440 }
441
442 #[test]
443 fn test_decision_status_display() {
444 assert_eq!(DecisionStatus::Pending.to_string(), "pending");
445 assert_eq!(DecisionStatus::InProgress.to_string(), "in_progress");
446 assert_eq!(DecisionStatus::Completed.to_string(), "completed");
447 assert_eq!(DecisionStatus::Overdue.to_string(), "overdue");
448 assert_eq!(DecisionStatus::Cancelled.to_string(), "cancelled");
449 }
450
451 #[test]
452 fn test_decision_status_from_str() {
453 assert_eq!(
454 "pending".parse::<DecisionStatus>().unwrap(),
455 DecisionStatus::Pending
456 );
457 assert_eq!(
458 "COMPLETED".parse::<DecisionStatus>().unwrap(),
459 DecisionStatus::Completed
460 );
461 assert_eq!(
462 "in_progress".parse::<DecisionStatus>().unwrap(),
463 DecisionStatus::InProgress
464 );
465
466 assert!("invalid".parse::<DecisionStatus>().is_err());
467 }
468}