koprogo_api/domain/entities/
meeting.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Type d'assemblée générale
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum MeetingType {
8    Ordinary,      // Assemblée Générale Ordinaire (AGO)
9    Extraordinary, // Assemblée Générale Extraordinaire (AGE)
10}
11
12/// Statut de l'assemblée
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub enum MeetingStatus {
15    Scheduled,
16    Completed,
17    Cancelled,
18}
19
20/// Représente une assemblée générale de copropriétaires
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub struct Meeting {
23    pub id: Uuid,
24    pub organization_id: Uuid,
25    pub building_id: Uuid,
26    pub meeting_type: MeetingType,
27    pub title: String,
28    pub description: Option<String>,
29    pub scheduled_date: DateTime<Utc>,
30    pub location: String,
31    pub status: MeetingStatus,
32    pub agenda: Vec<String>,
33    pub attendees_count: Option<i32>,
34    pub created_at: DateTime<Utc>,
35    pub updated_at: DateTime<Utc>,
36}
37
38impl Meeting {
39    pub fn new(
40        organization_id: Uuid,
41        building_id: Uuid,
42        meeting_type: MeetingType,
43        title: String,
44        description: Option<String>,
45        scheduled_date: DateTime<Utc>,
46        location: String,
47    ) -> Result<Self, String> {
48        if title.is_empty() {
49            return Err("Title cannot be empty".to_string());
50        }
51        if location.is_empty() {
52            return Err("Location cannot be empty".to_string());
53        }
54
55        let now = Utc::now();
56        Ok(Self {
57            id: Uuid::new_v4(),
58            organization_id,
59            building_id,
60            meeting_type,
61            title,
62            description,
63            scheduled_date,
64            location,
65            status: MeetingStatus::Scheduled,
66            agenda: Vec::new(),
67            attendees_count: None,
68            created_at: now,
69            updated_at: now,
70        })
71    }
72
73    pub fn add_agenda_item(&mut self, item: String) -> Result<(), String> {
74        if item.is_empty() {
75            return Err("Agenda item cannot be empty".to_string());
76        }
77        self.agenda.push(item);
78        self.updated_at = Utc::now();
79        Ok(())
80    }
81
82    pub fn complete(&mut self, attendees_count: i32) -> Result<(), String> {
83        match self.status {
84            MeetingStatus::Scheduled => {
85                self.status = MeetingStatus::Completed;
86                self.attendees_count = Some(attendees_count);
87                self.updated_at = Utc::now();
88                Ok(())
89            }
90            MeetingStatus::Completed => Err("Meeting is already completed".to_string()),
91            MeetingStatus::Cancelled => Err("Cannot complete a cancelled meeting".to_string()),
92        }
93    }
94
95    pub fn cancel(&mut self) -> Result<(), String> {
96        match self.status {
97            MeetingStatus::Scheduled => {
98                self.status = MeetingStatus::Cancelled;
99                self.updated_at = Utc::now();
100                Ok(())
101            }
102            MeetingStatus::Completed => Err("Cannot cancel a completed meeting".to_string()),
103            MeetingStatus::Cancelled => Err("Meeting is already cancelled".to_string()),
104        }
105    }
106
107    pub fn reschedule(&mut self, new_date: DateTime<Utc>) -> Result<(), String> {
108        match self.status {
109            MeetingStatus::Scheduled | MeetingStatus::Cancelled => {
110                self.scheduled_date = new_date;
111                self.status = MeetingStatus::Scheduled;
112                self.updated_at = Utc::now();
113                Ok(())
114            }
115            MeetingStatus::Completed => Err("Cannot reschedule a completed meeting".to_string()),
116        }
117    }
118
119    pub fn is_upcoming(&self) -> bool {
120        self.status == MeetingStatus::Scheduled && self.scheduled_date > Utc::now()
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use chrono::Duration;
128
129    #[test]
130    fn test_create_meeting_success() {
131        let org_id = Uuid::new_v4();
132        let building_id = Uuid::new_v4();
133        let future_date = Utc::now() + Duration::days(30);
134
135        let meeting = Meeting::new(
136            org_id,
137            building_id,
138            MeetingType::Ordinary,
139            "AGO 2024".to_string(),
140            Some("Assemblée générale ordinaire annuelle".to_string()),
141            future_date,
142            "Salle des fêtes".to_string(),
143        );
144
145        assert!(meeting.is_ok());
146        let meeting = meeting.unwrap();
147        assert_eq!(meeting.organization_id, org_id);
148        assert_eq!(meeting.status, MeetingStatus::Scheduled);
149        assert!(meeting.is_upcoming());
150    }
151
152    #[test]
153    fn test_add_agenda_item() {
154        let org_id = Uuid::new_v4();
155        let building_id = Uuid::new_v4();
156        let future_date = Utc::now() + Duration::days(30);
157
158        let mut meeting = Meeting::new(
159            org_id,
160            building_id,
161            MeetingType::Ordinary,
162            "AGO 2024".to_string(),
163            None,
164            future_date,
165            "Salle des fêtes".to_string(),
166        )
167        .unwrap();
168
169        let result = meeting.add_agenda_item("Approbation des comptes".to_string());
170        assert!(result.is_ok());
171        assert_eq!(meeting.agenda.len(), 1);
172    }
173
174    #[test]
175    fn test_complete_meeting() {
176        let org_id = Uuid::new_v4();
177        let building_id = Uuid::new_v4();
178        let future_date = Utc::now() + Duration::days(30);
179
180        let mut meeting = Meeting::new(
181            org_id,
182            building_id,
183            MeetingType::Ordinary,
184            "AGO 2024".to_string(),
185            None,
186            future_date,
187            "Salle des fêtes".to_string(),
188        )
189        .unwrap();
190
191        let result = meeting.complete(45);
192        assert!(result.is_ok());
193        assert_eq!(meeting.status, MeetingStatus::Completed);
194        assert_eq!(meeting.attendees_count, Some(45));
195        assert!(!meeting.is_upcoming());
196    }
197
198    #[test]
199    fn test_complete_already_completed_fails() {
200        let org_id = Uuid::new_v4();
201        let building_id = Uuid::new_v4();
202        let future_date = Utc::now() + Duration::days(30);
203
204        let mut meeting = Meeting::new(
205            org_id,
206            building_id,
207            MeetingType::Ordinary,
208            "AGO 2024".to_string(),
209            None,
210            future_date,
211            "Salle des fêtes".to_string(),
212        )
213        .unwrap();
214
215        meeting.complete(45).unwrap();
216        let result = meeting.complete(50);
217        assert!(result.is_err());
218        assert_eq!(meeting.attendees_count, Some(45)); // Should not change
219    }
220
221    #[test]
222    fn test_cancel_meeting() {
223        let org_id = Uuid::new_v4();
224        let building_id = Uuid::new_v4();
225        let future_date = Utc::now() + Duration::days(30);
226
227        let mut meeting = Meeting::new(
228            org_id,
229            building_id,
230            MeetingType::Ordinary,
231            "AGO 2024".to_string(),
232            None,
233            future_date,
234            "Salle des fêtes".to_string(),
235        )
236        .unwrap();
237
238        let result = meeting.cancel();
239        assert!(result.is_ok());
240        assert_eq!(meeting.status, MeetingStatus::Cancelled);
241    }
242
243    #[test]
244    fn test_reschedule_meeting() {
245        let org_id = Uuid::new_v4();
246        let building_id = Uuid::new_v4();
247        let future_date = Utc::now() + Duration::days(30);
248
249        let mut meeting = Meeting::new(
250            org_id,
251            building_id,
252            MeetingType::Ordinary,
253            "AGO 2024".to_string(),
254            None,
255            future_date,
256            "Salle des fêtes".to_string(),
257        )
258        .unwrap();
259
260        let new_date = Utc::now() + Duration::days(60);
261        let result = meeting.reschedule(new_date);
262        assert!(result.is_ok());
263        assert_eq!(meeting.scheduled_date, new_date);
264    }
265}