1use crate::application::dto::{
2 AddAgendaItemRequest, CompleteMeetingRequest, CreateMeetingRequest, MeetingResponse,
3 PageRequest, UpdateMeetingRequest,
4};
5use crate::application::ports::{MeetingCompletionCheckerPort, MeetingRepository};
6use crate::domain::entities::Meeting;
7use chrono::Duration;
8use rust_decimal::Decimal;
9#[cfg(test)]
10use rust_decimal_macros::dec;
11use std::sync::Arc;
12use uuid::Uuid;
13
14pub struct MeetingUseCases {
15 repository: Arc<dyn MeetingRepository>,
16 building_repository: Option<Arc<dyn crate::application::ports::BuildingRepository>>,
22 convocation_use_cases: Option<Arc<crate::application::use_cases::ConvocationUseCases>>,
23 completion_checker: Option<Arc<dyn MeetingCompletionCheckerPort>>,
27}
28
29impl MeetingUseCases {
30 pub fn new(repository: Arc<dyn MeetingRepository>) -> Self {
31 Self {
32 repository,
33 building_repository: None,
34 convocation_use_cases: None,
35 completion_checker: None,
36 }
37 }
38
39 pub fn new_with_convocation(
41 repository: Arc<dyn MeetingRepository>,
42 convocation_use_cases: Arc<crate::application::use_cases::ConvocationUseCases>,
43 ) -> Self {
44 Self {
45 repository,
46 building_repository: None,
47 convocation_use_cases: Some(convocation_use_cases),
48 completion_checker: None,
49 }
50 }
51
52 pub fn with_completion_checker(
56 mut self,
57 checker: Arc<dyn MeetingCompletionCheckerPort>,
58 ) -> Self {
59 self.completion_checker = Some(checker);
60 self
61 }
62
63 pub async fn build_completion_checklist(
75 &self,
76 meeting_id: Uuid,
77 ) -> Result<
78 (
79 crate::domain::entities::MeetingCompletionChecklist,
80 Vec<serde_json::Value>,
81 ),
82 String,
83 > {
84 let meeting = self
85 .repository
86 .find_by_id(meeting_id)
87 .await?
88 .ok_or_else(|| "Meeting not found".to_string())?;
89
90 let checker = self
91 .completion_checker
92 .as_ref()
93 .ok_or_else(|| "Completion checker not configured".to_string())?;
94
95 let checklist = checker.build_checklist(meeting_id).await?;
96
97 let missing_invariants = match meeting.assert_can_complete(&checklist) {
99 Ok(()) => Vec::new(),
100 Err(err) => err.missing,
101 };
102
103 use crate::domain::entities::MissingInvariant;
105 let missing_json: Vec<serde_json::Value> = missing_invariants
106 .iter()
107 .map(|m| match m {
108 MissingInvariant::ConvocationsNotSent => {
109 serde_json::json!({"type": "ConvocationsNotSent"})
110 }
111 MissingInvariant::VotesNotClosed { open_resolutions } => serde_json::json!({
112 "type": "VotesNotClosed",
113 "open_resolutions": open_resolutions,
114 }),
115 MissingInvariant::AttendanceNotRecorded => {
116 serde_json::json!({"type": "AttendanceNotRecorded"})
117 }
118 MissingInvariant::QuorumNotReached {
119 attended_quotas,
120 total_quotas,
121 } => serde_json::json!({
122 "type": "QuorumNotReached",
123 "attended_quotas": attended_quotas.to_string(),
124 "total_quotas": total_quotas.to_string(),
125 }),
126 MissingInvariant::HeadCountQuorumNotReached {
128 present_owners_count,
129 total_owners_count,
130 } => serde_json::json!({
131 "type": "HeadCountQuorumNotReached",
132 "present_owners_count": present_owners_count,
133 "total_owners_count": total_owners_count,
134 }),
135 MissingInvariant::MinutesDraftMissing => {
136 serde_json::json!({"type": "MinutesDraftMissing"})
137 }
138 })
139 .collect();
140
141 Ok((checklist, missing_json))
142 }
143
144 pub fn with_acp_resolution(
146 mut self,
147 building_repository: Arc<dyn crate::application::ports::BuildingRepository>,
148 ) -> Self {
149 self.building_repository = Some(building_repository);
150 self
151 }
152
153 async fn resoudre_lacp(&self, building_id: Uuid) -> Result<Uuid, String> {
158 let Some(building_repo) = &self.building_repository else {
159 return Err(
160 "Impossible de déterminer l'ACP dont c'est l'assemblée : dépôt d'immeubles \
161 non câblé"
162 .to_string(),
163 );
164 };
165 let building = building_repo
166 .find_by_id(building_id)
167 .await?
168 .ok_or_else(|| "Immeuble introuvable".to_string())?;
169 Ok(building.acp_id)
170 }
171
172 pub async fn create_meeting(
173 &self,
174 request: CreateMeetingRequest,
175 ) -> Result<MeetingResponse, String> {
176 let acp_id = self.resoudre_lacp(request.building_id).await?;
177 let mut meeting = Meeting::new(
178 acp_id,
179 request.organization_id,
180 request.building_id,
181 request.meeting_type,
182 request.title,
183 request.description,
184 request.scheduled_date,
185 request.location,
186 )?;
187
188 if request.is_second_convocation {
190 meeting.is_second_convocation = true;
191 }
192
193 let created = self.repository.create(&meeting).await?;
194 Ok(MeetingResponse::from(created))
195 }
196
197 pub async fn get_meeting(&self, id: Uuid) -> Result<Option<MeetingResponse>, String> {
198 let meeting = self.repository.find_by_id(id).await?;
199 Ok(meeting.map(MeetingResponse::from))
200 }
201
202 pub async fn list_meetings_by_building(
203 &self,
204 building_id: Uuid,
205 ) -> Result<Vec<MeetingResponse>, String> {
206 let meetings = self.repository.find_by_building(building_id).await?;
207 Ok(meetings.into_iter().map(MeetingResponse::from).collect())
208 }
209
210 pub async fn list_meetings_paginated(
211 &self,
212 page_request: &PageRequest,
213 organization_id: Option<Uuid>,
214 ) -> Result<(Vec<MeetingResponse>, i64), String> {
215 let (meetings, total) = self
216 .repository
217 .find_all_paginated(page_request, organization_id)
218 .await?;
219
220 let dtos = meetings.into_iter().map(MeetingResponse::from).collect();
221 Ok((dtos, total))
222 }
223
224 pub async fn update_meeting(
225 &self,
226 id: Uuid,
227 request: UpdateMeetingRequest,
228 ) -> Result<MeetingResponse, String> {
229 let mut meeting = self
230 .repository
231 .find_by_id(id)
232 .await?
233 .ok_or_else(|| "Meeting not found".to_string())?;
234
235 if let Some(title) = request.title {
237 if title.is_empty() {
238 return Err("Title cannot be empty".to_string());
239 }
240 meeting.title = title;
241 }
242
243 if let Some(description) = request.description {
244 meeting.description = Some(description);
245 }
246
247 if let Some(scheduled_date) = request.scheduled_date {
248 meeting.scheduled_date = scheduled_date;
249 }
250
251 if let Some(location) = request.location {
252 if location.is_empty() {
253 return Err("Location cannot be empty".to_string());
254 }
255 meeting.location = location;
256 }
257
258 meeting.updated_at = chrono::Utc::now();
259
260 let updated = self.repository.update(&meeting).await?;
261 Ok(MeetingResponse::from(updated))
262 }
263
264 pub async fn add_agenda_item(
265 &self,
266 id: Uuid,
267 request: AddAgendaItemRequest,
268 ) -> Result<MeetingResponse, String> {
269 let mut meeting = self
270 .repository
271 .find_by_id(id)
272 .await?
273 .ok_or_else(|| "Meeting not found".to_string())?;
274
275 meeting.add_agenda_item(request.item)?;
276
277 let updated = self.repository.update(&meeting).await?;
278 Ok(MeetingResponse::from(updated))
279 }
280
281 pub async fn complete_meeting(
299 &self,
300 id: Uuid,
301 request: CompleteMeetingRequest,
302 ) -> Result<MeetingResponse, String> {
303 let mut meeting = self
304 .repository
305 .find_by_id(id)
306 .await?
307 .ok_or_else(|| "Meeting not found".to_string())?;
308
309 if let Some(checker) = &self.completion_checker {
310 let checklist = checker.build_checklist(id).await?;
312 meeting.assert_can_complete(&checklist)?;
315 meeting.complete_internal()?;
316 let _ = request.attendees_count; use rust_decimal::prelude::ToPrimitive;
327 meeting.attendees_count = checklist.attended_quotas.to_i32();
328 } else {
329 #[allow(deprecated)]
333 meeting.complete(request.attendees_count)?;
334 }
335
336 let updated = self.repository.update(&meeting).await?;
337 Ok(MeetingResponse::from(updated))
338 }
339
340 pub async fn cancel_meeting(&self, id: Uuid) -> Result<MeetingResponse, String> {
341 let mut meeting = self
342 .repository
343 .find_by_id(id)
344 .await?
345 .ok_or_else(|| "Meeting not found".to_string())?;
346
347 meeting.cancel()?;
348
349 let updated = self.repository.update(&meeting).await?;
350 Ok(MeetingResponse::from(updated))
351 }
352
353 pub async fn reschedule_meeting(
354 &self,
355 id: Uuid,
356 new_date: chrono::DateTime<chrono::Utc>,
357 ) -> Result<MeetingResponse, String> {
358 let mut meeting = self
359 .repository
360 .find_by_id(id)
361 .await?
362 .ok_or_else(|| "Meeting not found".to_string())?;
363
364 meeting.reschedule(new_date)?;
365
366 let updated = self.repository.update(&meeting).await?;
367 Ok(MeetingResponse::from(updated))
368 }
369
370 pub async fn delete_meeting(&self, id: Uuid) -> Result<bool, String> {
371 self.repository.delete(id).await
372 }
373
374 pub async fn attach_minutes(
376 &self,
377 id: Uuid,
378 document_id: Uuid,
379 ) -> Result<MeetingResponse, String> {
380 let mut meeting = self
381 .repository
382 .find_by_id(id)
383 .await?
384 .ok_or_else(|| "Meeting not found".to_string())?;
385
386 meeting.set_minutes_sent(document_id)?;
387
388 let updated = self.repository.update(&meeting).await?;
389 Ok(MeetingResponse::from(updated))
390 }
391
392 pub async fn validate_quorum(
398 &self,
399 meeting_id: Uuid,
400 present_quotas: Decimal,
401 total_quotas: Decimal,
402 ) -> Result<(bool, MeetingResponse), String> {
403 let mut meeting = self
404 .repository
405 .find_by_id(meeting_id)
406 .await?
407 .ok_or_else(|| "Meeting not found".to_string())?;
408
409 let quorum_reached = meeting.validate_quorum(present_quotas, total_quotas)?;
410 let updated = self.repository.update(&meeting).await?;
411
412 if !quorum_reached {
414 if let Some(convocation_uc) = &self.convocation_use_cases {
415 let second_meeting_date = meeting.scheduled_date + Duration::days(15);
417 let second_meeting_id = Uuid::new_v4();
418
419 let _result = convocation_uc
421 .schedule_second_convocation(
422 meeting.organization_id,
423 meeting.building_id,
424 meeting_id,
425 second_meeting_id,
426 second_meeting_date,
427 "FR".to_string(),
428 Uuid::nil(), )
430 .await;
431 }
434 }
435
436 Ok((quorum_reached, MeetingResponse::from(updated)))
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use crate::application::dto::PageRequest;
444 use crate::application::ports::MeetingRepository;
445 use crate::domain::entities::{MeetingStatus, MeetingType};
446 use async_trait::async_trait;
447 use chrono::{Duration, Utc};
448 use mockall::mock;
449 use std::sync::Arc;
450
451 use crate::domain::entities::Building;
452
453 fn mock_building_repo() -> Arc<dyn crate::application::ports::BuildingRepository> {
459 mock_building_repo_pour(Uuid::new_v4())
460 }
461
462 fn mock_building_repo_pour(
463 acp_id: Uuid,
464 ) -> Arc<dyn crate::application::ports::BuildingRepository> {
465 struct Depot {
466 acp_id: Uuid,
467 }
468 impl Depot {
469 fn immeuble(&self) -> Building {
470 Building::new(
471 self.acp_id,
472 "Résidence du Parc".to_string(),
473 "12 Rue de la Loi".to_string(),
474 "Brussels".to_string(),
475 "1000".to_string(),
476 "Belgium".to_string(),
477 10,
478 1000,
479 Some(2015),
480 )
481 .expect("immeuble valide")
482 }
483 }
484 #[async_trait]
485 impl crate::application::ports::BuildingRepository for Depot {
486 async fn create(&self, b: &Building) -> Result<Building, String> {
487 Ok(b.clone())
488 }
489 async fn find_by_id(&self, _id: Uuid) -> Result<Option<Building>, String> {
490 Ok(Some(self.immeuble()))
491 }
492 async fn find_all(&self) -> Result<Vec<Building>, String> {
493 Ok(vec![self.immeuble()])
494 }
495 async fn find_all_paginated(
496 &self,
497 _p: &PageRequest,
498 _f: &crate::application::dto::BuildingFilters,
499 ) -> Result<(Vec<Building>, i64), String> {
500 Ok((vec![self.immeuble()], 1))
501 }
502 async fn update(&self, b: &Building) -> Result<Building, String> {
503 Ok(b.clone())
504 }
505 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
506 Ok(true)
507 }
508 async fn find_by_slug(&self, _s: &str) -> Result<Option<Building>, String> {
509 Ok(Some(self.immeuble()))
510 }
511 async fn find_by_id_with_metrics(
512 &self,
513 _id: Uuid,
514 ) -> Result<Option<(Building, crate::domain::entities::BuildingMetrics)>, String>
515 {
516 Ok(None)
517 }
518 }
519 Arc::new(Depot { acp_id })
520 }
521
522 mock! {
523 MeetingRepo {}
524
525 #[async_trait]
526 impl MeetingRepository for MeetingRepo {
527 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String>;
528 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String>;
529 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String>;
530 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String>;
531 async fn delete(&self, id: Uuid) -> Result<bool, String>;
532 async fn find_all_paginated(
533 &self,
534 page_request: &PageRequest,
535 organization_id: Option<Uuid>,
536 ) -> Result<(Vec<Meeting>, i64), String>;
537 }
538 }
539
540 fn make_meeting(building_id: Uuid, org_id: Uuid) -> Meeting {
542 Meeting::new(
543 Uuid::new_v4(), org_id,
545 building_id,
546 MeetingType::Ordinary,
547 "AGO 2024".to_string(),
548 Some("Annual general assembly".to_string()),
549 Utc::now() + Duration::days(30),
550 "Salle des fêtes".to_string(),
551 )
552 .unwrap()
553 }
554
555 #[tokio::test]
563 async fn test_lassemblee_est_celle_de_lacp_pas_du_syndic() {
564 let acp_de_limmeuble = Uuid::new_v4();
565 let cabinet_qui_la_tient = Uuid::new_v4();
566
567 let mut mock_repo = MockMeetingRepo::new();
568 mock_repo.expect_create().returning(|m| Ok(m.clone()));
569
570 let use_cases = MeetingUseCases::new(Arc::new(mock_repo))
571 .with_acp_resolution(mock_building_repo_pour(acp_de_limmeuble));
572
573 let assemblee = use_cases
574 .create_meeting(CreateMeetingRequest {
575 organization_id: cabinet_qui_la_tient,
576 building_id: Uuid::new_v4(),
577 meeting_type: MeetingType::Ordinary,
578 title: "AGO 2026".to_string(),
579 description: None,
580 scheduled_date: Utc::now() + Duration::days(30),
581 location: "Salle communale".to_string(),
582 is_second_convocation: false,
583 })
584 .await
585 .expect("création valide");
586
587 assert_eq!(
588 assemblee.acp_id, acp_de_limmeuble,
589 "l'assemblée est celle de l'ACP de l'immeuble"
590 );
591 assert_ne!(
595 assemblee.acp_id, cabinet_qui_la_tient,
596 "l'ACP et le syndic sont deux entités distinctes"
597 );
598 }
599
600 #[tokio::test]
602 async fn test_pas_dassemblee_sans_acp_identifiable() {
603 let mut mock_repo = MockMeetingRepo::new();
604 mock_repo.expect_create().returning(|m| Ok(m.clone()));
605
606 let use_cases = MeetingUseCases::new(Arc::new(mock_repo)); let erreur = use_cases
609 .create_meeting(CreateMeetingRequest {
610 organization_id: Uuid::new_v4(),
611 building_id: Uuid::new_v4(),
612 meeting_type: MeetingType::Ordinary,
613 title: "AGO 2026".to_string(),
614 description: None,
615 scheduled_date: Utc::now() + Duration::days(30),
616 location: "Salle communale".to_string(),
617 is_second_convocation: false,
618 })
619 .await
620 .expect_err("doit refuser");
621
622 assert!(
623 erreur.contains("ACP"),
624 "le refus doit nommer ce qui manque : {erreur}"
625 );
626 }
627
628 #[tokio::test]
629 async fn test_create_meeting_success() {
630 let mut mock_repo = MockMeetingRepo::new();
631
632 mock_repo.expect_create().returning(|m| Ok(m.clone()));
633
634 let use_cases =
635 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
636
637 let request = CreateMeetingRequest {
638 organization_id: Uuid::new_v4(),
639 building_id: Uuid::new_v4(),
640 meeting_type: MeetingType::Ordinary,
641 title: "AGO 2024".to_string(),
642 description: Some("Annual assembly".to_string()),
643 scheduled_date: Utc::now() + Duration::days(30),
644 location: "Salle communale".to_string(),
645 is_second_convocation: false,
646 };
647
648 let result = use_cases.create_meeting(request).await;
649 assert!(result.is_ok());
650 let response = result.unwrap();
651 assert_eq!(response.title, "AGO 2024");
652 assert_eq!(response.status, MeetingStatus::Scheduled);
653 }
654
655 #[tokio::test]
659 async fn test_create_meeting_empty_title_fails() {
660 let mock_repo = MockMeetingRepo::new();
661 let use_cases =
662 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
663
664 let request = CreateMeetingRequest {
665 organization_id: Uuid::new_v4(),
666 building_id: Uuid::new_v4(),
667 meeting_type: MeetingType::Ordinary,
668 title: "".to_string(),
669 description: None,
670 scheduled_date: Utc::now() + Duration::days(30),
671 location: "Salle communale".to_string(),
672 is_second_convocation: false,
673 };
674
675 let result = use_cases.create_meeting(request).await;
676 assert!(result.is_err());
677 assert!(result.unwrap_err().contains("Title cannot be empty"));
678 }
679
680 #[tokio::test]
684 async fn test_create_meeting_empty_location_fails() {
685 let mock_repo = MockMeetingRepo::new();
686 let use_cases =
687 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
688
689 let request = CreateMeetingRequest {
690 organization_id: Uuid::new_v4(),
691 building_id: Uuid::new_v4(),
692 meeting_type: MeetingType::Extraordinary,
693 title: "AGE 2024".to_string(),
694 description: None,
695 scheduled_date: Utc::now() + Duration::days(15),
696 location: "".to_string(),
697 is_second_convocation: false,
698 };
699
700 let result = use_cases.create_meeting(request).await;
701 assert!(result.is_err());
702 assert!(result.unwrap_err().contains("Location cannot be empty"));
703 }
704
705 #[tokio::test]
709 async fn test_get_meeting_found() {
710 let building_id = Uuid::new_v4();
711 let org_id = Uuid::new_v4();
712 let meeting = make_meeting(building_id, org_id);
713 let meeting_id = meeting.id;
714
715 let mut mock_repo = MockMeetingRepo::new();
716 mock_repo
717 .expect_find_by_id()
718 .withf(move |id| *id == meeting_id)
719 .returning(move |_| Ok(Some(make_meeting(building_id, org_id))));
720
721 let use_cases =
722 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
723
724 let result = use_cases.get_meeting(meeting_id).await;
725 assert!(result.is_ok());
726 assert!(result.unwrap().is_some());
727 }
728
729 #[tokio::test]
733 async fn test_get_meeting_not_found() {
734 let mut mock_repo = MockMeetingRepo::new();
735 mock_repo.expect_find_by_id().returning(|_| Ok(None));
736
737 let use_cases =
738 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
739
740 let result = use_cases.get_meeting(Uuid::new_v4()).await;
741 assert!(result.is_ok());
742 assert!(result.unwrap().is_none());
743 }
744
745 #[tokio::test]
749 async fn test_list_meetings_by_building() {
750 let building_id = Uuid::new_v4();
751 let org_id = Uuid::new_v4();
752
753 let mut mock_repo = MockMeetingRepo::new();
754 mock_repo
755 .expect_find_by_building()
756 .withf(move |id| *id == building_id)
757 .returning(move |_| {
758 Ok(vec![
759 make_meeting(building_id, org_id),
760 make_meeting(building_id, org_id),
761 ])
762 });
763
764 let use_cases =
765 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
766
767 let result = use_cases.list_meetings_by_building(building_id).await;
768 assert!(result.is_ok());
769 assert_eq!(result.unwrap().len(), 2);
770 }
771
772 #[tokio::test]
776 async fn test_update_meeting_success() {
777 let building_id = Uuid::new_v4();
778 let org_id = Uuid::new_v4();
779 let meeting = make_meeting(building_id, org_id);
780 let meeting_id = meeting.id;
781 let meeting_clone = meeting.clone();
782
783 let mut mock_repo = MockMeetingRepo::new();
784 mock_repo
785 .expect_find_by_id()
786 .withf(move |id| *id == meeting_id)
787 .returning(move |_| Ok(Some(meeting_clone.clone())));
788
789 mock_repo.expect_update().returning(|m| Ok(m.clone()));
790
791 let use_cases =
792 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
793
794 let request = UpdateMeetingRequest {
795 title: Some("Renamed AGO".to_string()),
796 description: Some("Updated description".to_string()),
797 scheduled_date: None,
798 location: None,
799 };
800
801 let result = use_cases.update_meeting(meeting_id, request).await;
802 assert!(result.is_ok());
803 let response = result.unwrap();
804 assert_eq!(response.title, "Renamed AGO");
805 }
806
807 #[tokio::test]
811 async fn test_update_meeting_not_found() {
812 let mut mock_repo = MockMeetingRepo::new();
813 mock_repo.expect_find_by_id().returning(|_| Ok(None));
814
815 let use_cases =
816 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
817
818 let request = UpdateMeetingRequest {
819 title: Some("New title".to_string()),
820 description: None,
821 scheduled_date: None,
822 location: None,
823 };
824
825 let result = use_cases.update_meeting(Uuid::new_v4(), request).await;
826 assert!(result.is_err());
827 assert!(result.unwrap_err().contains("Meeting not found"));
828 }
829
830 #[tokio::test]
834 async fn test_update_meeting_empty_title_rejected() {
835 let building_id = Uuid::new_v4();
836 let org_id = Uuid::new_v4();
837 let meeting = make_meeting(building_id, org_id);
838 let meeting_id = meeting.id;
839 let meeting_clone = meeting.clone();
840
841 let mut mock_repo = MockMeetingRepo::new();
842 mock_repo
843 .expect_find_by_id()
844 .returning(move |_| Ok(Some(meeting_clone.clone())));
845
846 let use_cases =
847 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
848
849 let request = UpdateMeetingRequest {
850 title: Some("".to_string()),
851 description: None,
852 scheduled_date: None,
853 location: None,
854 };
855
856 let result = use_cases.update_meeting(meeting_id, request).await;
857 assert!(result.is_err());
858 assert!(result.unwrap_err().contains("Title cannot be empty"));
859 }
860
861 #[tokio::test]
865 async fn test_delete_meeting_success() {
866 let meeting_id = Uuid::new_v4();
867
868 let mut mock_repo = MockMeetingRepo::new();
869 mock_repo
870 .expect_delete()
871 .withf(move |id| *id == meeting_id)
872 .returning(|_| Ok(true));
873
874 let use_cases =
875 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
876
877 let result = use_cases.delete_meeting(meeting_id).await;
878 assert!(result.is_ok());
879 assert!(result.unwrap());
880 }
881
882 #[tokio::test]
886 async fn test_validate_quorum_reached() {
887 let building_id = Uuid::new_v4();
888 let org_id = Uuid::new_v4();
889 let meeting = make_meeting(building_id, org_id);
890 let meeting_id = meeting.id;
891 let meeting_clone = meeting.clone();
892
893 let mut mock_repo = MockMeetingRepo::new();
894 mock_repo
895 .expect_find_by_id()
896 .withf(move |id| *id == meeting_id)
897 .returning(move |_| Ok(Some(meeting_clone.clone())));
898
899 mock_repo.expect_update().returning(|m| Ok(m.clone()));
900
901 let use_cases =
902 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
903
904 let result = use_cases
906 .validate_quorum(meeting_id, dec!(600), dec!(1000))
907 .await;
908 assert!(result.is_ok());
909 let (reached, response) = result.unwrap();
910 assert!(reached);
911 assert!(response.quorum_validated);
912 assert!((response.quorum_percentage.unwrap() - 60.0).abs() < 0.01);
913 }
914
915 #[tokio::test]
919 async fn test_validate_quorum_not_reached() {
920 let building_id = Uuid::new_v4();
921 let org_id = Uuid::new_v4();
922 let meeting = make_meeting(building_id, org_id);
923 let meeting_id = meeting.id;
924 let meeting_clone = meeting.clone();
925
926 let mut mock_repo = MockMeetingRepo::new();
927 mock_repo
928 .expect_find_by_id()
929 .withf(move |id| *id == meeting_id)
930 .returning(move |_| Ok(Some(meeting_clone.clone())));
931
932 mock_repo.expect_update().returning(|m| Ok(m.clone()));
933
934 let use_cases =
936 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
937
938 let result = use_cases
940 .validate_quorum(meeting_id, dec!(400), dec!(1000))
941 .await;
942 assert!(result.is_ok());
943 let (reached, response) = result.unwrap();
944 assert!(!reached);
945 assert!(!response.quorum_validated);
946 assert!((response.quorum_percentage.unwrap() - 40.0).abs() < 0.01);
947 }
948
949 #[tokio::test]
953 async fn test_validate_quorum_meeting_not_found() {
954 let mut mock_repo = MockMeetingRepo::new();
955 mock_repo.expect_find_by_id().returning(|_| Ok(None));
956
957 let use_cases =
958 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
959
960 let result = use_cases
961 .validate_quorum(Uuid::new_v4(), dec!(600), dec!(1000))
962 .await;
963 assert!(result.is_err());
964 assert!(result.unwrap_err().contains("Meeting not found"));
965 }
966
967 #[tokio::test]
971 async fn test_complete_meeting_success() {
972 let building_id = Uuid::new_v4();
973 let org_id = Uuid::new_v4();
974 let meeting = make_meeting(building_id, org_id);
975 let meeting_id = meeting.id;
976 let meeting_clone = meeting.clone();
977
978 let mut mock_repo = MockMeetingRepo::new();
979 mock_repo
980 .expect_find_by_id()
981 .withf(move |id| *id == meeting_id)
982 .returning(move |_| Ok(Some(meeting_clone.clone())));
983
984 mock_repo.expect_update().returning(|m| Ok(m.clone()));
985
986 let use_cases =
987 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
988
989 let request = CompleteMeetingRequest {
990 attendees_count: 42,
991 };
992 let result = use_cases.complete_meeting(meeting_id, request).await;
993 assert!(result.is_ok());
994 let response = result.unwrap();
995 assert_eq!(response.status, MeetingStatus::Completed);
996 assert_eq!(response.attendees_count, Some(42));
997 }
998
999 #[tokio::test]
1003 async fn test_cancel_meeting_success() {
1004 let building_id = Uuid::new_v4();
1005 let org_id = Uuid::new_v4();
1006 let meeting = make_meeting(building_id, org_id);
1007 let meeting_id = meeting.id;
1008 let meeting_clone = meeting.clone();
1009
1010 let mut mock_repo = MockMeetingRepo::new();
1011 mock_repo
1012 .expect_find_by_id()
1013 .withf(move |id| *id == meeting_id)
1014 .returning(move |_| Ok(Some(meeting_clone.clone())));
1015
1016 mock_repo.expect_update().returning(|m| Ok(m.clone()));
1017
1018 let use_cases =
1019 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
1020
1021 let result = use_cases.cancel_meeting(meeting_id).await;
1022 assert!(result.is_ok());
1023 assert_eq!(result.unwrap().status, MeetingStatus::Cancelled);
1024 }
1025
1026 #[tokio::test]
1030 async fn test_add_agenda_item_success() {
1031 let building_id = Uuid::new_v4();
1032 let org_id = Uuid::new_v4();
1033 let meeting = make_meeting(building_id, org_id);
1034 let meeting_id = meeting.id;
1035 let meeting_clone = meeting.clone();
1036
1037 let mut mock_repo = MockMeetingRepo::new();
1038 mock_repo
1039 .expect_find_by_id()
1040 .withf(move |id| *id == meeting_id)
1041 .returning(move |_| Ok(Some(meeting_clone.clone())));
1042
1043 mock_repo.expect_update().returning(|m| Ok(m.clone()));
1044
1045 let use_cases =
1046 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
1047
1048 let request = AddAgendaItemRequest {
1049 item: "Approbation des comptes".to_string(),
1050 };
1051 let result = use_cases.add_agenda_item(meeting_id, request).await;
1052 assert!(result.is_ok());
1053 let response = result.unwrap();
1054 assert_eq!(response.agenda.len(), 1);
1055 assert_eq!(response.agenda[0], "Approbation des comptes");
1056 }
1057
1058 #[tokio::test]
1062 async fn test_validate_quorum_exact_50_percent_not_reached() {
1063 let building_id = Uuid::new_v4();
1064 let org_id = Uuid::new_v4();
1065 let meeting = make_meeting(building_id, org_id);
1066 let meeting_id = meeting.id;
1067 let meeting_clone = meeting.clone();
1068
1069 let mut mock_repo = MockMeetingRepo::new();
1070 mock_repo
1071 .expect_find_by_id()
1072 .withf(move |id| *id == meeting_id)
1073 .returning(move |_| Ok(Some(meeting_clone.clone())));
1074
1075 mock_repo.expect_update().returning(|m| Ok(m.clone()));
1076
1077 let use_cases =
1078 MeetingUseCases::new(Arc::new(mock_repo)).with_acp_resolution(mock_building_repo());
1079
1080 let result = use_cases
1082 .validate_quorum(meeting_id, dec!(500), dec!(1000))
1083 .await;
1084 assert!(result.is_ok());
1085 let (reached, response) = result.unwrap();
1086 assert!(!reached);
1087 assert!(!response.quorum_validated);
1088 assert!((response.quorum_percentage.unwrap() - 50.0).abs() < 0.01);
1089 }
1090}