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