1use crate::application::dto::{
2 AddAgendaItemRequest, CompleteMeetingRequest, CreateMeetingRequest, MeetingResponse,
3 PageRequest, UpdateMeetingRequest,
4};
5use crate::application::ports::MeetingRepository;
6use crate::domain::entities::Meeting;
7use chrono::Duration;
8use std::sync::Arc;
9use uuid::Uuid;
10
11pub struct MeetingUseCases {
12 repository: Arc<dyn MeetingRepository>,
13 convocation_use_cases: Option<Arc<crate::application::use_cases::ConvocationUseCases>>,
14}
15
16impl MeetingUseCases {
17 pub fn new(repository: Arc<dyn MeetingRepository>) -> Self {
18 Self {
19 repository,
20 convocation_use_cases: None,
21 }
22 }
23
24 pub fn new_with_convocation(
26 repository: Arc<dyn MeetingRepository>,
27 convocation_use_cases: Arc<crate::application::use_cases::ConvocationUseCases>,
28 ) -> Self {
29 Self {
30 repository,
31 convocation_use_cases: Some(convocation_use_cases),
32 }
33 }
34
35 pub async fn create_meeting(
36 &self,
37 request: CreateMeetingRequest,
38 ) -> Result<MeetingResponse, String> {
39 let meeting = Meeting::new(
40 request.organization_id,
41 request.building_id,
42 request.meeting_type,
43 request.title,
44 request.description,
45 request.scheduled_date,
46 request.location,
47 )?;
48
49 let created = self.repository.create(&meeting).await?;
50 Ok(MeetingResponse::from(created))
51 }
52
53 pub async fn get_meeting(&self, id: Uuid) -> Result<Option<MeetingResponse>, String> {
54 let meeting = self.repository.find_by_id(id).await?;
55 Ok(meeting.map(MeetingResponse::from))
56 }
57
58 pub async fn list_meetings_by_building(
59 &self,
60 building_id: Uuid,
61 ) -> Result<Vec<MeetingResponse>, String> {
62 let meetings = self.repository.find_by_building(building_id).await?;
63 Ok(meetings.into_iter().map(MeetingResponse::from).collect())
64 }
65
66 pub async fn list_meetings_paginated(
67 &self,
68 page_request: &PageRequest,
69 organization_id: Option<Uuid>,
70 ) -> Result<(Vec<MeetingResponse>, i64), String> {
71 let (meetings, total) = self
72 .repository
73 .find_all_paginated(page_request, organization_id)
74 .await?;
75
76 let dtos = meetings.into_iter().map(MeetingResponse::from).collect();
77 Ok((dtos, total))
78 }
79
80 pub async fn update_meeting(
81 &self,
82 id: Uuid,
83 request: UpdateMeetingRequest,
84 ) -> Result<MeetingResponse, String> {
85 let mut meeting = self
86 .repository
87 .find_by_id(id)
88 .await?
89 .ok_or_else(|| "Meeting not found".to_string())?;
90
91 if let Some(title) = request.title {
93 if title.is_empty() {
94 return Err("Title cannot be empty".to_string());
95 }
96 meeting.title = title;
97 }
98
99 if let Some(description) = request.description {
100 meeting.description = Some(description);
101 }
102
103 if let Some(scheduled_date) = request.scheduled_date {
104 meeting.scheduled_date = scheduled_date;
105 }
106
107 if let Some(location) = request.location {
108 if location.is_empty() {
109 return Err("Location cannot be empty".to_string());
110 }
111 meeting.location = location;
112 }
113
114 meeting.updated_at = chrono::Utc::now();
115
116 let updated = self.repository.update(&meeting).await?;
117 Ok(MeetingResponse::from(updated))
118 }
119
120 pub async fn add_agenda_item(
121 &self,
122 id: Uuid,
123 request: AddAgendaItemRequest,
124 ) -> Result<MeetingResponse, String> {
125 let mut meeting = self
126 .repository
127 .find_by_id(id)
128 .await?
129 .ok_or_else(|| "Meeting not found".to_string())?;
130
131 meeting.add_agenda_item(request.item)?;
132
133 let updated = self.repository.update(&meeting).await?;
134 Ok(MeetingResponse::from(updated))
135 }
136
137 pub async fn complete_meeting(
138 &self,
139 id: Uuid,
140 request: CompleteMeetingRequest,
141 ) -> Result<MeetingResponse, String> {
142 let mut meeting = self
143 .repository
144 .find_by_id(id)
145 .await?
146 .ok_or_else(|| "Meeting not found".to_string())?;
147
148 meeting.complete(request.attendees_count)?;
149
150 let updated = self.repository.update(&meeting).await?;
151 Ok(MeetingResponse::from(updated))
152 }
153
154 pub async fn cancel_meeting(&self, id: Uuid) -> Result<MeetingResponse, String> {
155 let mut meeting = self
156 .repository
157 .find_by_id(id)
158 .await?
159 .ok_or_else(|| "Meeting not found".to_string())?;
160
161 meeting.cancel()?;
162
163 let updated = self.repository.update(&meeting).await?;
164 Ok(MeetingResponse::from(updated))
165 }
166
167 pub async fn reschedule_meeting(
168 &self,
169 id: Uuid,
170 new_date: chrono::DateTime<chrono::Utc>,
171 ) -> Result<MeetingResponse, String> {
172 let mut meeting = self
173 .repository
174 .find_by_id(id)
175 .await?
176 .ok_or_else(|| "Meeting not found".to_string())?;
177
178 meeting.reschedule(new_date)?;
179
180 let updated = self.repository.update(&meeting).await?;
181 Ok(MeetingResponse::from(updated))
182 }
183
184 pub async fn delete_meeting(&self, id: Uuid) -> Result<bool, String> {
185 self.repository.delete(id).await
186 }
187
188 pub async fn attach_minutes(
190 &self,
191 id: Uuid,
192 document_id: Uuid,
193 ) -> Result<MeetingResponse, String> {
194 let mut meeting = self
195 .repository
196 .find_by_id(id)
197 .await?
198 .ok_or_else(|| "Meeting not found".to_string())?;
199
200 meeting.set_minutes_sent(document_id)?;
201
202 let updated = self.repository.update(&meeting).await?;
203 Ok(MeetingResponse::from(updated))
204 }
205
206 pub async fn validate_quorum(
212 &self,
213 meeting_id: Uuid,
214 present_quotas: f64,
215 total_quotas: f64,
216 ) -> Result<(bool, MeetingResponse), String> {
217 let mut meeting = self
218 .repository
219 .find_by_id(meeting_id)
220 .await?
221 .ok_or_else(|| "Meeting not found".to_string())?;
222
223 let quorum_reached = meeting.validate_quorum(present_quotas, total_quotas)?;
224 let updated = self.repository.update(&meeting).await?;
225
226 if !quorum_reached {
228 if let Some(convocation_uc) = &self.convocation_use_cases {
229 let second_meeting_date = meeting.scheduled_date + Duration::days(15);
231 let second_meeting_id = Uuid::new_v4();
232
233 let _result = convocation_uc
235 .schedule_second_convocation(
236 meeting.organization_id,
237 meeting.building_id,
238 meeting_id,
239 second_meeting_id,
240 second_meeting_date,
241 "FR".to_string(),
242 Uuid::nil(), )
244 .await;
245 }
248 }
249
250 Ok((quorum_reached, MeetingResponse::from(updated)))
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::application::dto::PageRequest;
258 use crate::application::ports::MeetingRepository;
259 use crate::domain::entities::{MeetingStatus, MeetingType};
260 use async_trait::async_trait;
261 use chrono::{Duration, Utc};
262 use mockall::mock;
263 use std::sync::Arc;
264
265 mock! {
266 MeetingRepo {}
267
268 #[async_trait]
269 impl MeetingRepository for MeetingRepo {
270 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String>;
271 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String>;
272 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String>;
273 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String>;
274 async fn delete(&self, id: Uuid) -> Result<bool, String>;
275 async fn find_all_paginated(
276 &self,
277 page_request: &PageRequest,
278 organization_id: Option<Uuid>,
279 ) -> Result<(Vec<Meeting>, i64), String>;
280 }
281 }
282
283 fn make_meeting(building_id: Uuid, org_id: Uuid) -> Meeting {
285 Meeting::new(
286 org_id,
287 building_id,
288 MeetingType::Ordinary,
289 "AGO 2024".to_string(),
290 Some("Annual general assembly".to_string()),
291 Utc::now() + Duration::days(30),
292 "Salle des fêtes".to_string(),
293 )
294 .unwrap()
295 }
296
297 #[tokio::test]
301 async fn test_create_meeting_success() {
302 let mut mock_repo = MockMeetingRepo::new();
303
304 mock_repo.expect_create().returning(|m| Ok(m.clone()));
305
306 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
307
308 let request = CreateMeetingRequest {
309 organization_id: Uuid::new_v4(),
310 building_id: Uuid::new_v4(),
311 meeting_type: MeetingType::Ordinary,
312 title: "AGO 2024".to_string(),
313 description: Some("Annual assembly".to_string()),
314 scheduled_date: Utc::now() + Duration::days(30),
315 location: "Salle communale".to_string(),
316 };
317
318 let result = use_cases.create_meeting(request).await;
319 assert!(result.is_ok());
320 let response = result.unwrap();
321 assert_eq!(response.title, "AGO 2024");
322 assert_eq!(response.status, MeetingStatus::Scheduled);
323 }
324
325 #[tokio::test]
329 async fn test_create_meeting_empty_title_fails() {
330 let mock_repo = MockMeetingRepo::new();
331 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
332
333 let request = CreateMeetingRequest {
334 organization_id: Uuid::new_v4(),
335 building_id: Uuid::new_v4(),
336 meeting_type: MeetingType::Ordinary,
337 title: "".to_string(),
338 description: None,
339 scheduled_date: Utc::now() + Duration::days(30),
340 location: "Salle communale".to_string(),
341 };
342
343 let result = use_cases.create_meeting(request).await;
344 assert!(result.is_err());
345 assert!(result.unwrap_err().contains("Title cannot be empty"));
346 }
347
348 #[tokio::test]
352 async fn test_create_meeting_empty_location_fails() {
353 let mock_repo = MockMeetingRepo::new();
354 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
355
356 let request = CreateMeetingRequest {
357 organization_id: Uuid::new_v4(),
358 building_id: Uuid::new_v4(),
359 meeting_type: MeetingType::Extraordinary,
360 title: "AGE 2024".to_string(),
361 description: None,
362 scheduled_date: Utc::now() + Duration::days(15),
363 location: "".to_string(),
364 };
365
366 let result = use_cases.create_meeting(request).await;
367 assert!(result.is_err());
368 assert!(result.unwrap_err().contains("Location cannot be empty"));
369 }
370
371 #[tokio::test]
375 async fn test_get_meeting_found() {
376 let building_id = Uuid::new_v4();
377 let org_id = Uuid::new_v4();
378 let meeting = make_meeting(building_id, org_id);
379 let meeting_id = meeting.id;
380
381 let mut mock_repo = MockMeetingRepo::new();
382 mock_repo
383 .expect_find_by_id()
384 .withf(move |id| *id == meeting_id)
385 .returning(move |_| Ok(Some(make_meeting(building_id, org_id))));
386
387 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
388
389 let result = use_cases.get_meeting(meeting_id).await;
390 assert!(result.is_ok());
391 assert!(result.unwrap().is_some());
392 }
393
394 #[tokio::test]
398 async fn test_get_meeting_not_found() {
399 let mut mock_repo = MockMeetingRepo::new();
400 mock_repo.expect_find_by_id().returning(|_| Ok(None));
401
402 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
403
404 let result = use_cases.get_meeting(Uuid::new_v4()).await;
405 assert!(result.is_ok());
406 assert!(result.unwrap().is_none());
407 }
408
409 #[tokio::test]
413 async fn test_list_meetings_by_building() {
414 let building_id = Uuid::new_v4();
415 let org_id = Uuid::new_v4();
416
417 let mut mock_repo = MockMeetingRepo::new();
418 mock_repo
419 .expect_find_by_building()
420 .withf(move |id| *id == building_id)
421 .returning(move |_| {
422 Ok(vec![
423 make_meeting(building_id, org_id),
424 make_meeting(building_id, org_id),
425 ])
426 });
427
428 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
429
430 let result = use_cases.list_meetings_by_building(building_id).await;
431 assert!(result.is_ok());
432 assert_eq!(result.unwrap().len(), 2);
433 }
434
435 #[tokio::test]
439 async fn test_update_meeting_success() {
440 let building_id = Uuid::new_v4();
441 let org_id = Uuid::new_v4();
442 let meeting = make_meeting(building_id, org_id);
443 let meeting_id = meeting.id;
444 let meeting_clone = meeting.clone();
445
446 let mut mock_repo = MockMeetingRepo::new();
447 mock_repo
448 .expect_find_by_id()
449 .withf(move |id| *id == meeting_id)
450 .returning(move |_| Ok(Some(meeting_clone.clone())));
451
452 mock_repo.expect_update().returning(|m| Ok(m.clone()));
453
454 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
455
456 let request = UpdateMeetingRequest {
457 title: Some("Renamed AGO".to_string()),
458 description: Some("Updated description".to_string()),
459 scheduled_date: None,
460 location: None,
461 };
462
463 let result = use_cases.update_meeting(meeting_id, request).await;
464 assert!(result.is_ok());
465 let response = result.unwrap();
466 assert_eq!(response.title, "Renamed AGO");
467 }
468
469 #[tokio::test]
473 async fn test_update_meeting_not_found() {
474 let mut mock_repo = MockMeetingRepo::new();
475 mock_repo.expect_find_by_id().returning(|_| Ok(None));
476
477 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
478
479 let request = UpdateMeetingRequest {
480 title: Some("New title".to_string()),
481 description: None,
482 scheduled_date: None,
483 location: None,
484 };
485
486 let result = use_cases.update_meeting(Uuid::new_v4(), request).await;
487 assert!(result.is_err());
488 assert!(result.unwrap_err().contains("Meeting not found"));
489 }
490
491 #[tokio::test]
495 async fn test_update_meeting_empty_title_rejected() {
496 let building_id = Uuid::new_v4();
497 let org_id = Uuid::new_v4();
498 let meeting = make_meeting(building_id, org_id);
499 let meeting_id = meeting.id;
500 let meeting_clone = meeting.clone();
501
502 let mut mock_repo = MockMeetingRepo::new();
503 mock_repo
504 .expect_find_by_id()
505 .returning(move |_| Ok(Some(meeting_clone.clone())));
506
507 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
508
509 let request = UpdateMeetingRequest {
510 title: Some("".to_string()),
511 description: None,
512 scheduled_date: None,
513 location: None,
514 };
515
516 let result = use_cases.update_meeting(meeting_id, request).await;
517 assert!(result.is_err());
518 assert!(result.unwrap_err().contains("Title cannot be empty"));
519 }
520
521 #[tokio::test]
525 async fn test_delete_meeting_success() {
526 let meeting_id = Uuid::new_v4();
527
528 let mut mock_repo = MockMeetingRepo::new();
529 mock_repo
530 .expect_delete()
531 .withf(move |id| *id == meeting_id)
532 .returning(|_| Ok(true));
533
534 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
535
536 let result = use_cases.delete_meeting(meeting_id).await;
537 assert!(result.is_ok());
538 assert!(result.unwrap());
539 }
540
541 #[tokio::test]
545 async fn test_validate_quorum_reached() {
546 let building_id = Uuid::new_v4();
547 let org_id = Uuid::new_v4();
548 let meeting = make_meeting(building_id, org_id);
549 let meeting_id = meeting.id;
550 let meeting_clone = meeting.clone();
551
552 let mut mock_repo = MockMeetingRepo::new();
553 mock_repo
554 .expect_find_by_id()
555 .withf(move |id| *id == meeting_id)
556 .returning(move |_| Ok(Some(meeting_clone.clone())));
557
558 mock_repo.expect_update().returning(|m| Ok(m.clone()));
559
560 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
561
562 let result = use_cases.validate_quorum(meeting_id, 600.0, 1000.0).await;
564 assert!(result.is_ok());
565 let (reached, response) = result.unwrap();
566 assert!(reached);
567 assert!(response.quorum_validated);
568 assert!((response.quorum_percentage.unwrap() - 60.0).abs() < 0.01);
569 }
570
571 #[tokio::test]
575 async fn test_validate_quorum_not_reached() {
576 let building_id = Uuid::new_v4();
577 let org_id = Uuid::new_v4();
578 let meeting = make_meeting(building_id, org_id);
579 let meeting_id = meeting.id;
580 let meeting_clone = meeting.clone();
581
582 let mut mock_repo = MockMeetingRepo::new();
583 mock_repo
584 .expect_find_by_id()
585 .withf(move |id| *id == meeting_id)
586 .returning(move |_| Ok(Some(meeting_clone.clone())));
587
588 mock_repo.expect_update().returning(|m| Ok(m.clone()));
589
590 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
592
593 let result = use_cases.validate_quorum(meeting_id, 400.0, 1000.0).await;
595 assert!(result.is_ok());
596 let (reached, response) = result.unwrap();
597 assert!(!reached);
598 assert!(!response.quorum_validated);
599 assert!((response.quorum_percentage.unwrap() - 40.0).abs() < 0.01);
600 }
601
602 #[tokio::test]
606 async fn test_validate_quorum_meeting_not_found() {
607 let mut mock_repo = MockMeetingRepo::new();
608 mock_repo.expect_find_by_id().returning(|_| Ok(None));
609
610 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
611
612 let result = use_cases
613 .validate_quorum(Uuid::new_v4(), 600.0, 1000.0)
614 .await;
615 assert!(result.is_err());
616 assert!(result.unwrap_err().contains("Meeting not found"));
617 }
618
619 #[tokio::test]
623 async fn test_complete_meeting_success() {
624 let building_id = Uuid::new_v4();
625 let org_id = Uuid::new_v4();
626 let meeting = make_meeting(building_id, org_id);
627 let meeting_id = meeting.id;
628 let meeting_clone = meeting.clone();
629
630 let mut mock_repo = MockMeetingRepo::new();
631 mock_repo
632 .expect_find_by_id()
633 .withf(move |id| *id == meeting_id)
634 .returning(move |_| Ok(Some(meeting_clone.clone())));
635
636 mock_repo.expect_update().returning(|m| Ok(m.clone()));
637
638 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
639
640 let request = CompleteMeetingRequest {
641 attendees_count: 42,
642 };
643 let result = use_cases.complete_meeting(meeting_id, request).await;
644 assert!(result.is_ok());
645 let response = result.unwrap();
646 assert_eq!(response.status, MeetingStatus::Completed);
647 assert_eq!(response.attendees_count, Some(42));
648 }
649
650 #[tokio::test]
654 async fn test_cancel_meeting_success() {
655 let building_id = Uuid::new_v4();
656 let org_id = Uuid::new_v4();
657 let meeting = make_meeting(building_id, org_id);
658 let meeting_id = meeting.id;
659 let meeting_clone = meeting.clone();
660
661 let mut mock_repo = MockMeetingRepo::new();
662 mock_repo
663 .expect_find_by_id()
664 .withf(move |id| *id == meeting_id)
665 .returning(move |_| Ok(Some(meeting_clone.clone())));
666
667 mock_repo.expect_update().returning(|m| Ok(m.clone()));
668
669 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
670
671 let result = use_cases.cancel_meeting(meeting_id).await;
672 assert!(result.is_ok());
673 assert_eq!(result.unwrap().status, MeetingStatus::Cancelled);
674 }
675
676 #[tokio::test]
680 async fn test_add_agenda_item_success() {
681 let building_id = Uuid::new_v4();
682 let org_id = Uuid::new_v4();
683 let meeting = make_meeting(building_id, org_id);
684 let meeting_id = meeting.id;
685 let meeting_clone = meeting.clone();
686
687 let mut mock_repo = MockMeetingRepo::new();
688 mock_repo
689 .expect_find_by_id()
690 .withf(move |id| *id == meeting_id)
691 .returning(move |_| Ok(Some(meeting_clone.clone())));
692
693 mock_repo.expect_update().returning(|m| Ok(m.clone()));
694
695 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
696
697 let request = AddAgendaItemRequest {
698 item: "Approbation des comptes".to_string(),
699 };
700 let result = use_cases.add_agenda_item(meeting_id, request).await;
701 assert!(result.is_ok());
702 let response = result.unwrap();
703 assert_eq!(response.agenda.len(), 1);
704 assert_eq!(response.agenda[0], "Approbation des comptes");
705 }
706
707 #[tokio::test]
711 async fn test_validate_quorum_exact_50_percent_not_reached() {
712 let building_id = Uuid::new_v4();
713 let org_id = Uuid::new_v4();
714 let meeting = make_meeting(building_id, org_id);
715 let meeting_id = meeting.id;
716 let meeting_clone = meeting.clone();
717
718 let mut mock_repo = MockMeetingRepo::new();
719 mock_repo
720 .expect_find_by_id()
721 .withf(move |id| *id == meeting_id)
722 .returning(move |_| Ok(Some(meeting_clone.clone())));
723
724 mock_repo.expect_update().returning(|m| Ok(m.clone()));
725
726 let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
727
728 let result = use_cases.validate_quorum(meeting_id, 500.0, 1000.0).await;
730 assert!(result.is_ok());
731 let (reached, response) = result.unwrap();
732 assert!(!reached);
733 assert!(!response.quorum_validated);
734 assert!((response.quorum_percentage.unwrap() - 50.0).abs() < 0.01);
735 }
736}