koprogo_api/application/use_cases/
meeting_use_cases.rs

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    /// Create MeetingUseCases with ConvocationUseCases for automatic 2nd convocation scheduling
25    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        // Art. 3.87 §5 CC: 2nd convocation skips quorum requirement
50        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        // Update fields if provided
97        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    /// Attach minutes document to a completed meeting (Issue #313)
194    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    /// Valide le quorum d'une AG (Art. 3.87 §5 CC).
212    /// Doit être appelé AVANT que les votes soient ouverts.
213    /// Si quorum non atteint, déclenche automatiquement la création d'une 2e convocation
214    /// pour le même bâtiment (si ConvocationUseCases disponible).
215    /// Retourne Ok(true) si quorum atteint, Ok(false) si 2e convocation requise.
216    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        // Art. 3.87 §5 CC: Si quorum non atteint, déclencher une 2e convocation
232        if !quorum_reached {
233            if let Some(convocation_uc) = &self.convocation_use_cases {
234                // Create second meeting (15 days after first)
235                let second_meeting_date = meeting.scheduled_date + Duration::days(15);
236                let second_meeting_id = Uuid::new_v4();
237
238                // Schedule second convocation (language defaults to FR)
239                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(), // system-created convocation
248                    )
249                    .await;
250                // Note: We don't fail if second convocation scheduling fails
251                // (could log the error, but don't block the quorum validation result)
252            }
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    /// Helper to build a valid Meeting for testing purposes.
289    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    // ---------------------------------------------------------------
303    // 1. Create meeting success
304    // ---------------------------------------------------------------
305    #[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    // ---------------------------------------------------------------
332    // 2. Create meeting with invalid data (empty title)
333    // ---------------------------------------------------------------
334    #[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    // ---------------------------------------------------------------
356    // 3. Create meeting with empty location
357    // ---------------------------------------------------------------
358    #[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    // ---------------------------------------------------------------
380    // 4. Get meeting by ID — found
381    // ---------------------------------------------------------------
382    #[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    // ---------------------------------------------------------------
403    // 5. Get meeting by ID — not found
404    // ---------------------------------------------------------------
405    #[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    // ---------------------------------------------------------------
418    // 6. List meetings by building
419    // ---------------------------------------------------------------
420    #[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    // ---------------------------------------------------------------
444    // 7. Update meeting — success
445    // ---------------------------------------------------------------
446    #[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    // ---------------------------------------------------------------
478    // 8. Update meeting — not found
479    // ---------------------------------------------------------------
480    #[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    // ---------------------------------------------------------------
500    // 9. Update meeting — empty title rejected
501    // ---------------------------------------------------------------
502    #[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    // ---------------------------------------------------------------
530    // 10. Delete meeting
531    // ---------------------------------------------------------------
532    #[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    // ---------------------------------------------------------------
550    // 11. Validate quorum — reached (>50%)
551    // ---------------------------------------------------------------
552    #[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        // 600/1000 = 60% → quorum reached
571        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    // ---------------------------------------------------------------
580    // 12. Validate quorum — not reached (<=50%)
581    // ---------------------------------------------------------------
582    #[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        // No convocation_use_cases set, so second convocation won't be triggered
599        let use_cases = MeetingUseCases::new(Arc::new(mock_repo));
600
601        // 400/1000 = 40% → quorum NOT reached
602        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    // ---------------------------------------------------------------
611    // 13. Validate quorum — meeting not found
612    // ---------------------------------------------------------------
613    #[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    // ---------------------------------------------------------------
628    // 14. Complete meeting via use case
629    // ---------------------------------------------------------------
630    #[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    // ---------------------------------------------------------------
659    // 15. Cancel meeting via use case
660    // ---------------------------------------------------------------
661    #[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    // ---------------------------------------------------------------
685    // 16. Add agenda item via use case
686    // ---------------------------------------------------------------
687    #[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    // ---------------------------------------------------------------
716    // 17. Validate quorum — exact 50% NOT reached (Art. 3.87 §5: >50% strict)
717    // ---------------------------------------------------------------
718    #[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        // 500/1000 = exactly 50% → quorum NOT reached (Art. 3.87 §5: strictly >50%)
737        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}