1use crate::application::dto::{
2 ConvocationRecipientResponse, ConvocationResponse, CreateConvocationRequest,
3 RecipientTrackingSummaryResponse, ScheduleConvocationRequest, SendConvocationRequest,
4};
5use crate::application::ports::{
6 BuildingRepository, ConvocationRecipientRepository, ConvocationRepository, MeetingRepository,
7 OwnerRepository,
8};
9use crate::domain::entities::{AttendanceStatus, Convocation, ConvocationRecipient};
10use crate::domain::services::ConvocationExporter;
11use chrono::Utc;
12use std::sync::Arc;
13use uuid::Uuid;
14
15pub struct ConvocationUseCases {
16 convocation_repository: Arc<dyn ConvocationRepository>,
17 recipient_repository: Arc<dyn ConvocationRecipientRepository>,
18 owner_repository: Arc<dyn OwnerRepository>,
19 building_repository: Arc<dyn BuildingRepository>,
20 meeting_repository: Arc<dyn MeetingRepository>,
21}
22
23impl ConvocationUseCases {
24 pub fn new(
25 convocation_repository: Arc<dyn ConvocationRepository>,
26 recipient_repository: Arc<dyn ConvocationRecipientRepository>,
27 owner_repository: Arc<dyn OwnerRepository>,
28 building_repository: Arc<dyn BuildingRepository>,
29 meeting_repository: Arc<dyn MeetingRepository>,
30 ) -> Self {
31 Self {
32 convocation_repository,
33 recipient_repository,
34 owner_repository,
35 building_repository,
36 meeting_repository,
37 }
38 }
39
40 pub async fn create_convocation(
42 &self,
43 organization_id: Uuid,
44 request: CreateConvocationRequest,
45 created_by: Uuid,
46 ) -> Result<ConvocationResponse, String> {
47 let convocation = Convocation::new(
49 organization_id,
50 request.building_id,
51 request.meeting_id,
52 request.meeting_type,
53 request.meeting_date,
54 request.language,
55 created_by,
56 )?;
57
58 let created = self.convocation_repository.create(&convocation).await?;
59
60 Ok(ConvocationResponse::from(created))
61 }
62
63 pub async fn get_convocation(&self, id: Uuid) -> Result<ConvocationResponse, String> {
65 let convocation = self
66 .convocation_repository
67 .find_by_id(id)
68 .await?
69 .ok_or_else(|| format!("Convocation not found: {}", id))?;
70
71 Ok(ConvocationResponse::from(convocation))
72 }
73
74 pub async fn get_convocation_by_meeting(
76 &self,
77 meeting_id: Uuid,
78 ) -> Result<Option<ConvocationResponse>, String> {
79 let convocation = self
80 .convocation_repository
81 .find_by_meeting_id(meeting_id)
82 .await?;
83
84 Ok(convocation.map(ConvocationResponse::from))
85 }
86
87 pub async fn list_building_convocations(
89 &self,
90 building_id: Uuid,
91 ) -> Result<Vec<ConvocationResponse>, String> {
92 let convocations = self
93 .convocation_repository
94 .find_by_building(building_id)
95 .await?;
96
97 Ok(convocations
98 .into_iter()
99 .map(ConvocationResponse::from)
100 .collect())
101 }
102
103 pub async fn list_organization_convocations(
105 &self,
106 organization_id: Uuid,
107 ) -> Result<Vec<ConvocationResponse>, String> {
108 let convocations = self
109 .convocation_repository
110 .find_by_organization(organization_id)
111 .await?;
112
113 Ok(convocations
114 .into_iter()
115 .map(ConvocationResponse::from)
116 .collect())
117 }
118
119 pub async fn schedule_convocation(
121 &self,
122 id: Uuid,
123 request: ScheduleConvocationRequest,
124 ) -> Result<ConvocationResponse, String> {
125 let mut convocation = self
126 .convocation_repository
127 .find_by_id(id)
128 .await?
129 .ok_or_else(|| format!("Convocation not found: {}", id))?;
130
131 convocation.schedule(request.send_date)?;
132
133 let updated = self.convocation_repository.update(&convocation).await?;
134
135 Ok(ConvocationResponse::from(updated))
136 }
137
138 pub async fn send_convocation(
141 &self,
142 id: Uuid,
143 request: SendConvocationRequest,
144 ) -> Result<ConvocationResponse, String> {
145 let mut convocation = self
146 .convocation_repository
147 .find_by_id(id)
148 .await?
149 .ok_or_else(|| format!("Convocation not found: {}", id))?;
150
151 let building = self
153 .building_repository
154 .find_by_id(convocation.building_id)
155 .await?
156 .ok_or_else(|| format!("Building not found: {}", convocation.building_id))?;
157
158 let meeting = self
160 .meeting_repository
161 .find_by_id(convocation.meeting_id)
162 .await?
163 .ok_or_else(|| format!("Meeting not found: {}", convocation.meeting_id))?;
164
165 let pdf_bytes = ConvocationExporter::export_to_pdf(&building, &meeting, &convocation)
167 .map_err(|e| format!("Failed to generate PDF: {}", e))?;
168
169 let upload_dir =
171 std::env::var("UPLOAD_DIR").unwrap_or_else(|_| "/tmp/koprogo-uploads".to_string());
172 let pdf_file_path = format!("{}/convocations/conv-{}.pdf", upload_dir, id);
173 ConvocationExporter::save_to_file(&pdf_bytes, &pdf_file_path)
174 .map_err(|e| format!("Failed to save PDF: {}", e))?;
175
176 let mut recipients = Vec::new();
178 for owner_id in &request.recipient_owner_ids {
179 let owner = self
180 .owner_repository
181 .find_by_id(*owner_id)
182 .await?
183 .ok_or_else(|| format!("Owner not found: {}", owner_id))?;
184
185 let mut recipient = ConvocationRecipient::new(id, *owner_id, owner.email)?;
186 recipient.mark_email_sent();
187 recipients.push(recipient);
188 }
189
190 let created_recipients = self.recipient_repository.create_many(&recipients).await?;
192
193 convocation.mark_sent(pdf_file_path, created_recipients.len() as i32)?;
195
196 let updated = self.convocation_repository.update(&convocation).await?;
197
198 Ok(ConvocationResponse::from(updated))
199 }
200
201 pub async fn mark_recipient_email_sent(
203 &self,
204 recipient_id: Uuid,
205 ) -> Result<ConvocationRecipientResponse, String> {
206 let mut recipient = self
207 .recipient_repository
208 .find_by_id(recipient_id)
209 .await?
210 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
211
212 recipient.mark_email_sent();
213
214 let updated = self.recipient_repository.update(&recipient).await?;
215
216 Ok(ConvocationRecipientResponse::from(updated))
217 }
218
219 pub async fn mark_recipient_email_opened(
221 &self,
222 recipient_id: Uuid,
223 ) -> Result<ConvocationRecipientResponse, String> {
224 let mut recipient = self
225 .recipient_repository
226 .find_by_id(recipient_id)
227 .await?
228 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
229
230 recipient.mark_email_opened()?;
231
232 let updated = self.recipient_repository.update(&recipient).await?;
233
234 self.update_convocation_tracking(recipient.convocation_id)
236 .await?;
237
238 Ok(ConvocationRecipientResponse::from(updated))
239 }
240
241 pub async fn update_recipient_attendance(
243 &self,
244 recipient_id: Uuid,
245 status: AttendanceStatus,
246 ) -> Result<ConvocationRecipientResponse, String> {
247 let mut recipient = self
248 .recipient_repository
249 .find_by_id(recipient_id)
250 .await?
251 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
252
253 recipient.update_attendance_status(status)?;
254
255 let updated = self.recipient_repository.update(&recipient).await?;
256
257 self.update_convocation_tracking(recipient.convocation_id)
259 .await?;
260
261 Ok(ConvocationRecipientResponse::from(updated))
262 }
263
264 pub async fn set_recipient_proxy(
266 &self,
267 recipient_id: Uuid,
268 proxy_owner_id: Uuid,
269 ) -> Result<ConvocationRecipientResponse, String> {
270 let mut recipient = self
271 .recipient_repository
272 .find_by_id(recipient_id)
273 .await?
274 .ok_or_else(|| format!("Recipient not found: {}", recipient_id))?;
275
276 recipient.set_proxy(proxy_owner_id)?;
277
278 let updated = self.recipient_repository.update(&recipient).await?;
279
280 Ok(ConvocationRecipientResponse::from(updated))
281 }
282
283 pub async fn send_reminders(
286 &self,
287 convocation_id: Uuid,
288 ) -> Result<Vec<ConvocationRecipientResponse>, String> {
289 let recipients = self
291 .recipient_repository
292 .find_needing_reminder(convocation_id)
293 .await?;
294
295 let mut updated_recipients = Vec::new();
296
297 for mut recipient in recipients {
298 recipient.mark_reminder_sent()?;
299 let updated = self.recipient_repository.update(&recipient).await?;
300 updated_recipients.push(ConvocationRecipientResponse::from(updated));
301 }
302
303 if !updated_recipients.is_empty() {
305 let mut convocation = self
306 .convocation_repository
307 .find_by_id(convocation_id)
308 .await?
309 .ok_or_else(|| format!("Convocation not found: {}", convocation_id))?;
310
311 convocation.mark_reminder_sent()?;
312 self.convocation_repository.update(&convocation).await?;
313 }
314
315 Ok(updated_recipients)
316 }
317
318 pub async fn get_tracking_summary(
320 &self,
321 convocation_id: Uuid,
322 ) -> Result<RecipientTrackingSummaryResponse, String> {
323 let summary = self
324 .recipient_repository
325 .get_tracking_summary(convocation_id)
326 .await?;
327
328 Ok(RecipientTrackingSummaryResponse::new(
329 summary.total_count,
330 summary.opened_count,
331 summary.will_attend_count,
332 summary.will_not_attend_count,
333 summary.attended_count,
334 summary.did_not_attend_count,
335 summary.pending_count,
336 summary.failed_email_count,
337 ))
338 }
339
340 pub async fn list_convocation_recipients(
342 &self,
343 convocation_id: Uuid,
344 ) -> Result<Vec<ConvocationRecipientResponse>, String> {
345 let recipients = self
346 .recipient_repository
347 .find_by_convocation(convocation_id)
348 .await?;
349
350 Ok(recipients
351 .into_iter()
352 .map(ConvocationRecipientResponse::from)
353 .collect())
354 }
355
356 pub async fn cancel_convocation(&self, id: Uuid) -> Result<ConvocationResponse, String> {
358 let mut convocation = self
359 .convocation_repository
360 .find_by_id(id)
361 .await?
362 .ok_or_else(|| format!("Convocation not found: {}", id))?;
363
364 convocation.cancel()?;
365
366 let updated = self.convocation_repository.update(&convocation).await?;
367
368 Ok(ConvocationResponse::from(updated))
369 }
370
371 pub async fn delete_convocation(&self, id: Uuid) -> Result<bool, String> {
373 self.convocation_repository.delete(id).await
374 }
375
376 pub async fn process_scheduled_convocations(&self) -> Result<Vec<ConvocationResponse>, String> {
379 let now = Utc::now();
380 let scheduled = self
381 .convocation_repository
382 .find_pending_scheduled(now)
383 .await?;
384
385 let mut sent = Vec::new();
386
387 for convocation in scheduled {
388 sent.push(ConvocationResponse::from(convocation));
391 }
392
393 Ok(sent)
394 }
395
396 pub async fn process_reminder_sending(&self) -> Result<Vec<ConvocationResponse>, String> {
399 let now = Utc::now();
400 let needing_reminder = self
401 .convocation_repository
402 .find_needing_reminder(now)
403 .await?;
404
405 let mut processed = Vec::new();
406
407 for convocation in needing_reminder {
408 self.send_reminders(convocation.id).await?;
410 processed.push(ConvocationResponse::from(convocation));
411 }
412
413 Ok(processed)
414 }
415
416 pub async fn schedule_second_convocation(
429 &self,
430 organization_id: Uuid,
431 building_id: Uuid,
432 first_meeting_id: Uuid,
433 new_meeting_id: Uuid,
434 new_meeting_date: chrono::DateTime<chrono::Utc>,
435 language: String,
436 created_by: Uuid,
437 ) -> Result<ConvocationResponse, String> {
438 let first_meeting = self
440 .meeting_repository
441 .find_by_id(first_meeting_id)
442 .await?
443 .ok_or_else(|| format!("First meeting not found: {}", first_meeting_id))?;
444
445 let second_convocation = Convocation::new_second_convocation(
448 organization_id,
449 building_id,
450 new_meeting_id,
451 first_meeting_id,
452 first_meeting.scheduled_date,
453 new_meeting_date,
454 language,
455 created_by,
456 )?;
457
458 let created = self
459 .convocation_repository
460 .create(&second_convocation)
461 .await?;
462
463 Ok(ConvocationResponse::from(created))
464 }
465
466 async fn update_convocation_tracking(&self, convocation_id: Uuid) -> Result<(), String> {
468 let summary = self
469 .recipient_repository
470 .get_tracking_summary(convocation_id)
471 .await?;
472
473 let mut convocation = self
474 .convocation_repository
475 .find_by_id(convocation_id)
476 .await?
477 .ok_or_else(|| format!("Convocation not found: {}", convocation_id))?;
478
479 convocation.update_tracking_counts(
480 summary.opened_count as i32,
481 summary.will_attend_count as i32,
482 summary.will_not_attend_count as i32,
483 );
484
485 self.convocation_repository.update(&convocation).await?;
486
487 Ok(())
488 }
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494 use crate::application::dto::{BuildingFilters, OwnerFilters, PageRequest};
495 use crate::application::ports::{
496 ConvocationRecipientRepository, ConvocationRepository, RecipientTrackingSummary,
497 };
498 use crate::domain::entities::{
499 AttendanceStatus, Building, Convocation, ConvocationRecipient, ConvocationStatus,
500 ConvocationType, Meeting, Owner,
501 };
502 use async_trait::async_trait;
503 use chrono::{Duration, Utc};
504 use mockall::mock;
505 use std::sync::Arc;
506 use uuid::Uuid;
507
508 mock! {
513 ConvRepo {}
514
515 #[async_trait]
516 impl ConvocationRepository for ConvRepo {
517 async fn create(&self, convocation: &Convocation) -> Result<Convocation, String>;
518 async fn find_by_id(&self, id: Uuid) -> Result<Option<Convocation>, String>;
519 async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Option<Convocation>, String>;
520 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Convocation>, String>;
521 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Convocation>, String>;
522 async fn find_by_status(&self, organization_id: Uuid, status: ConvocationStatus) -> Result<Vec<Convocation>, String>;
523 async fn find_pending_scheduled(&self, now: chrono::DateTime<Utc>) -> Result<Vec<Convocation>, String>;
524 async fn find_needing_reminder(&self, now: chrono::DateTime<Utc>) -> Result<Vec<Convocation>, String>;
525 async fn update(&self, convocation: &Convocation) -> Result<Convocation, String>;
526 async fn delete(&self, id: Uuid) -> Result<bool, String>;
527 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String>;
528 async fn count_by_status(&self, organization_id: Uuid, status: ConvocationStatus) -> Result<i64, String>;
529 }
530 }
531
532 mock! {
533 RecipientRepo {}
534
535 #[async_trait]
536 impl ConvocationRecipientRepository for RecipientRepo {
537 async fn create(&self, recipient: &ConvocationRecipient) -> Result<ConvocationRecipient, String>;
538 async fn create_many(&self, recipients: &[ConvocationRecipient]) -> Result<Vec<ConvocationRecipient>, String>;
539 async fn find_by_id(&self, id: Uuid) -> Result<Option<ConvocationRecipient>, String>;
540 async fn find_by_convocation(&self, convocation_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
541 async fn find_by_convocation_and_owner(&self, convocation_id: Uuid, owner_id: Uuid) -> Result<Option<ConvocationRecipient>, String>;
542 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
543 async fn find_by_attendance_status(&self, convocation_id: Uuid, status: AttendanceStatus) -> Result<Vec<ConvocationRecipient>, String>;
544 async fn find_needing_reminder(&self, convocation_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
545 async fn find_failed_emails(&self, convocation_id: Uuid) -> Result<Vec<ConvocationRecipient>, String>;
546 async fn update(&self, recipient: &ConvocationRecipient) -> Result<ConvocationRecipient, String>;
547 async fn delete(&self, id: Uuid) -> Result<bool, String>;
548 async fn count_by_convocation(&self, convocation_id: Uuid) -> Result<i64, String>;
549 async fn count_opened(&self, convocation_id: Uuid) -> Result<i64, String>;
550 async fn count_by_attendance_status(&self, convocation_id: Uuid, status: AttendanceStatus) -> Result<i64, String>;
551 async fn get_tracking_summary(&self, convocation_id: Uuid) -> Result<RecipientTrackingSummary, String>;
552 }
553 }
554
555 mock! {
556 OwnerRepo {}
557
558 #[async_trait]
559 impl OwnerRepository for OwnerRepo {
560 async fn create(&self, owner: &Owner) -> Result<Owner, String>;
561 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String>;
562 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String>;
563 async fn find_by_user_id_and_organization(&self, user_id: Uuid, organization_id: Uuid) -> Result<Option<Owner>, String>;
564 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String>;
565 async fn find_all(&self) -> Result<Vec<Owner>, String>;
566 async fn find_all_paginated(&self, page_request: &PageRequest, filters: &OwnerFilters) -> Result<(Vec<Owner>, i64), String>;
567 async fn update(&self, owner: &Owner) -> Result<Owner, String>;
568 async fn delete(&self, id: Uuid) -> Result<bool, String>;
569 async fn set_user_link(&self, owner_id: Uuid, user_id: Option<Uuid>) -> Result<bool, String>;
570 }
571 }
572
573 mock! {
574 BuildingRepo {}
575
576 #[async_trait]
577 impl BuildingRepository for BuildingRepo {
578 async fn create(&self, building: &Building) -> Result<Building, String>;
579 async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
580 async fn find_all(&self) -> Result<Vec<Building>, String>;
581 async fn find_all_paginated(&self, page_request: &PageRequest, filters: &BuildingFilters) -> Result<(Vec<Building>, i64), String>;
582 async fn update(&self, building: &Building) -> Result<Building, String>;
583 async fn delete(&self, id: Uuid) -> Result<bool, String>;
584 async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
585 }
586 }
587
588 mock! {
589 MeetingRepo {}
590
591 #[async_trait]
592 impl MeetingRepository for MeetingRepo {
593 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String>;
594 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String>;
595 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String>;
596 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String>;
597 async fn delete(&self, id: Uuid) -> Result<bool, String>;
598 async fn find_all_paginated(&self, page_request: &PageRequest, organization_id: Option<Uuid>) -> Result<(Vec<Meeting>, i64), String>;
599 }
600 }
601
602 fn make_use_cases(
608 conv_repo: MockConvRepo,
609 recip_repo: MockRecipientRepo,
610 owner_repo: MockOwnerRepo,
611 building_repo: MockBuildingRepo,
612 meeting_repo: MockMeetingRepo,
613 ) -> ConvocationUseCases {
614 ConvocationUseCases::new(
615 Arc::new(conv_repo),
616 Arc::new(recip_repo),
617 Arc::new(owner_repo),
618 Arc::new(building_repo),
619 Arc::new(meeting_repo),
620 )
621 }
622
623 fn make_convocation(org_id: Uuid, building_id: Uuid, meeting_id: Uuid) -> Convocation {
625 let meeting_date = Utc::now() + Duration::days(20);
626 Convocation::new(
627 org_id,
628 building_id,
629 meeting_id,
630 ConvocationType::Ordinary,
631 meeting_date,
632 "FR".to_string(),
633 Uuid::new_v4(),
634 )
635 .expect("helper should produce a valid convocation")
636 }
637
638 fn make_sent_convocation(org_id: Uuid, building_id: Uuid, meeting_id: Uuid) -> Convocation {
640 let mut conv = make_convocation(org_id, building_id, meeting_id);
641 conv.mark_sent("/tmp/conv.pdf".to_string(), 5).unwrap();
642 conv
643 }
644
645 fn make_recipient(convocation_id: Uuid, owner_id: Uuid) -> ConvocationRecipient {
647 let mut r =
648 ConvocationRecipient::new(convocation_id, owner_id, "owner@example.com".to_string())
649 .unwrap();
650 r.mark_email_sent();
651 r
652 }
653
654 #[tokio::test]
658 async fn test_create_convocation_ordinary_valid_deadline() {
659 let org_id = Uuid::new_v4();
660 let building_id = Uuid::new_v4();
661 let meeting_id = Uuid::new_v4();
662 let meeting_date = Utc::now() + Duration::days(20);
663
664 let mut conv_repo = MockConvRepo::new();
665 conv_repo.expect_create().returning(|conv| Ok(conv.clone()));
666
667 let uc = make_use_cases(
668 conv_repo,
669 MockRecipientRepo::new(),
670 MockOwnerRepo::new(),
671 MockBuildingRepo::new(),
672 MockMeetingRepo::new(),
673 );
674
675 let request = CreateConvocationRequest {
676 building_id,
677 meeting_id,
678 meeting_type: ConvocationType::Ordinary,
679 meeting_date,
680 language: "FR".to_string(),
681 };
682
683 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
684
685 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
686 let resp = result.unwrap();
687 assert_eq!(resp.status, ConvocationStatus::Draft);
688 assert_eq!(resp.language, "FR");
689 assert!(resp.respects_legal_deadline);
690 }
691
692 #[tokio::test]
697 async fn test_create_convocation_violating_legal_deadline() {
698 let org_id = Uuid::new_v4();
699 let meeting_date = Utc::now() + Duration::days(5); let uc = make_use_cases(
702 MockConvRepo::new(),
703 MockRecipientRepo::new(),
704 MockOwnerRepo::new(),
705 MockBuildingRepo::new(),
706 MockMeetingRepo::new(),
707 );
708
709 let request = CreateConvocationRequest {
710 building_id: Uuid::new_v4(),
711 meeting_id: Uuid::new_v4(),
712 meeting_type: ConvocationType::Ordinary,
713 meeting_date,
714 language: "FR".to_string(),
715 };
716
717 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
718
719 assert!(result.is_err());
720 let err = result.unwrap_err();
721 assert!(
722 err.contains("Meeting date too soon"),
723 "Expected 'Meeting date too soon' error, got: {}",
724 err
725 );
726 }
727
728 #[tokio::test]
733 async fn test_create_convocation_extraordinary_valid_deadline() {
734 let org_id = Uuid::new_v4();
735 let meeting_date = Utc::now() + Duration::days(16); let mut conv_repo = MockConvRepo::new();
738 conv_repo.expect_create().returning(|conv| Ok(conv.clone()));
739
740 let uc = make_use_cases(
741 conv_repo,
742 MockRecipientRepo::new(),
743 MockOwnerRepo::new(),
744 MockBuildingRepo::new(),
745 MockMeetingRepo::new(),
746 );
747
748 let request = CreateConvocationRequest {
749 building_id: Uuid::new_v4(),
750 meeting_id: Uuid::new_v4(),
751 meeting_type: ConvocationType::Extraordinary,
752 meeting_date,
753 language: "NL".to_string(),
754 };
755
756 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
757
758 assert!(result.is_ok());
759 let resp = result.unwrap();
760 assert_eq!(resp.language, "NL");
761 }
762
763 #[tokio::test]
767 async fn test_schedule_convocation_success() {
768 let org_id = Uuid::new_v4();
769 let building_id = Uuid::new_v4();
770 let meeting_id = Uuid::new_v4();
771 let conv = make_convocation(org_id, building_id, meeting_id);
772 let conv_id = conv.id;
773 let min_send_date = conv.minimum_send_date;
774
775 let mut conv_repo = MockConvRepo::new();
776 let conv_clone = conv.clone();
777 conv_repo
778 .expect_find_by_id()
779 .returning(move |_| Ok(Some(conv_clone.clone())));
780 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
781
782 let uc = make_use_cases(
783 conv_repo,
784 MockRecipientRepo::new(),
785 MockOwnerRepo::new(),
786 MockBuildingRepo::new(),
787 MockMeetingRepo::new(),
788 );
789
790 let send_date = min_send_date - Duration::days(1);
792 let request = ScheduleConvocationRequest { send_date };
793
794 let result = uc.schedule_convocation(conv_id, request).await;
795
796 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
797 let resp = result.unwrap();
798 assert_eq!(resp.status, ConvocationStatus::Scheduled);
799 assert!(resp.scheduled_send_date.is_some());
800 }
801
802 #[tokio::test]
806 async fn test_cancel_convocation_success() {
807 let org_id = Uuid::new_v4();
808 let building_id = Uuid::new_v4();
809 let meeting_id = Uuid::new_v4();
810 let conv = make_convocation(org_id, building_id, meeting_id);
811 let conv_id = conv.id;
812
813 let mut conv_repo = MockConvRepo::new();
814 let conv_clone = conv.clone();
815 conv_repo
816 .expect_find_by_id()
817 .returning(move |_| Ok(Some(conv_clone.clone())));
818 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
819
820 let uc = make_use_cases(
821 conv_repo,
822 MockRecipientRepo::new(),
823 MockOwnerRepo::new(),
824 MockBuildingRepo::new(),
825 MockMeetingRepo::new(),
826 );
827
828 let result = uc.cancel_convocation(conv_id).await;
829
830 assert!(result.is_ok());
831 let resp = result.unwrap();
832 assert_eq!(resp.status, ConvocationStatus::Cancelled);
833 }
834
835 #[tokio::test]
839 async fn test_cancel_convocation_already_cancelled_error() {
840 let org_id = Uuid::new_v4();
841 let building_id = Uuid::new_v4();
842 let meeting_id = Uuid::new_v4();
843 let mut conv = make_convocation(org_id, building_id, meeting_id);
844 conv.cancel().unwrap(); let conv_id = conv.id;
846
847 let mut conv_repo = MockConvRepo::new();
848 let conv_clone = conv.clone();
849 conv_repo
850 .expect_find_by_id()
851 .returning(move |_| Ok(Some(conv_clone.clone())));
852
853 let uc = make_use_cases(
854 conv_repo,
855 MockRecipientRepo::new(),
856 MockOwnerRepo::new(),
857 MockBuildingRepo::new(),
858 MockMeetingRepo::new(),
859 );
860
861 let result = uc.cancel_convocation(conv_id).await;
862
863 assert!(result.is_err());
864 assert!(result.unwrap_err().contains("already cancelled"));
865 }
866
867 #[tokio::test]
871 async fn test_send_reminders_marks_recipients() {
872 let org_id = Uuid::new_v4();
873 let building_id = Uuid::new_v4();
874 let meeting_id = Uuid::new_v4();
875 let conv = make_sent_convocation(org_id, building_id, meeting_id);
876 let conv_id = conv.id;
877
878 let owner1_id = Uuid::new_v4();
879 let owner2_id = Uuid::new_v4();
880 let r1 = make_recipient(conv_id, owner1_id);
881 let r2 = make_recipient(conv_id, owner2_id);
882
883 let mut recip_repo = MockRecipientRepo::new();
884 let r1_clone = r1.clone();
885 let r2_clone = r2.clone();
886 recip_repo
887 .expect_find_needing_reminder()
888 .returning(move |_| Ok(vec![r1_clone.clone(), r2_clone.clone()]));
889 recip_repo.expect_update().returning(|r| Ok(r.clone()));
890
891 let mut conv_repo = MockConvRepo::new();
892 let conv_clone = conv.clone();
893 conv_repo
894 .expect_find_by_id()
895 .returning(move |_| Ok(Some(conv_clone.clone())));
896 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
897
898 let uc = make_use_cases(
899 conv_repo,
900 recip_repo,
901 MockOwnerRepo::new(),
902 MockBuildingRepo::new(),
903 MockMeetingRepo::new(),
904 );
905
906 let result = uc.send_reminders(conv_id).await;
907
908 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
909 let recipients = result.unwrap();
910 assert_eq!(recipients.len(), 2);
911 assert!(recipients[0].reminder_sent_at.is_some());
913 assert!(recipients[1].reminder_sent_at.is_some());
914 }
915
916 #[tokio::test]
920 async fn test_mark_email_opened_updates_tracking() {
921 let org_id = Uuid::new_v4();
922 let building_id = Uuid::new_v4();
923 let meeting_id = Uuid::new_v4();
924 let conv = make_sent_convocation(org_id, building_id, meeting_id);
925 let conv_id = conv.id;
926
927 let owner_id = Uuid::new_v4();
928 let recipient = make_recipient(conv_id, owner_id);
929 let recipient_id = recipient.id;
930
931 let mut recip_repo = MockRecipientRepo::new();
932 let recip_clone = recipient.clone();
933 recip_repo
934 .expect_find_by_id()
935 .returning(move |_| Ok(Some(recip_clone.clone())));
936 recip_repo.expect_update().returning(|r| Ok(r.clone()));
937 recip_repo
938 .expect_get_tracking_summary()
939 .returning(move |_| {
940 Ok(RecipientTrackingSummary {
941 total_count: 5,
942 opened_count: 3,
943 will_attend_count: 2,
944 will_not_attend_count: 1,
945 attended_count: 0,
946 did_not_attend_count: 0,
947 pending_count: 2,
948 failed_email_count: 0,
949 })
950 });
951
952 let mut conv_repo = MockConvRepo::new();
953 let conv_clone = conv.clone();
954 conv_repo
955 .expect_find_by_id()
956 .returning(move |_| Ok(Some(conv_clone.clone())));
957 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
958
959 let uc = make_use_cases(
960 conv_repo,
961 recip_repo,
962 MockOwnerRepo::new(),
963 MockBuildingRepo::new(),
964 MockMeetingRepo::new(),
965 );
966
967 let result = uc.mark_recipient_email_opened(recipient_id).await;
968
969 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
970 let resp = result.unwrap();
971 assert!(resp.has_opened_email);
972 }
973
974 #[tokio::test]
978 async fn test_update_attendance_will_attend() {
979 let org_id = Uuid::new_v4();
980 let building_id = Uuid::new_v4();
981 let meeting_id = Uuid::new_v4();
982 let conv = make_sent_convocation(org_id, building_id, meeting_id);
983 let conv_id = conv.id;
984
985 let owner_id = Uuid::new_v4();
986 let recipient = make_recipient(conv_id, owner_id);
987 let recipient_id = recipient.id;
988
989 let mut recip_repo = MockRecipientRepo::new();
990 let recip_clone = recipient.clone();
991 recip_repo
992 .expect_find_by_id()
993 .returning(move |_| Ok(Some(recip_clone.clone())));
994 recip_repo.expect_update().returning(|r| Ok(r.clone()));
995 recip_repo
996 .expect_get_tracking_summary()
997 .returning(move |_| {
998 Ok(RecipientTrackingSummary {
999 total_count: 5,
1000 opened_count: 1,
1001 will_attend_count: 1,
1002 will_not_attend_count: 0,
1003 attended_count: 0,
1004 did_not_attend_count: 0,
1005 pending_count: 4,
1006 failed_email_count: 0,
1007 })
1008 });
1009
1010 let mut conv_repo = MockConvRepo::new();
1011 let conv_clone = conv.clone();
1012 conv_repo
1013 .expect_find_by_id()
1014 .returning(move |_| Ok(Some(conv_clone.clone())));
1015 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1016
1017 let uc = make_use_cases(
1018 conv_repo,
1019 recip_repo,
1020 MockOwnerRepo::new(),
1021 MockBuildingRepo::new(),
1022 MockMeetingRepo::new(),
1023 );
1024
1025 let result = uc
1026 .update_recipient_attendance(recipient_id, AttendanceStatus::WillAttend)
1027 .await;
1028
1029 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1030 let resp = result.unwrap();
1031 assert_eq!(resp.attendance_status, AttendanceStatus::WillAttend);
1032 assert!(resp.has_confirmed_attendance);
1033 }
1034
1035 #[tokio::test]
1039 async fn test_set_proxy_delegation() {
1040 let conv_id = Uuid::new_v4();
1041 let owner_id = Uuid::new_v4();
1042 let proxy_owner_id = Uuid::new_v4();
1043 let recipient = make_recipient(conv_id, owner_id);
1044 let recipient_id = recipient.id;
1045
1046 let mut recip_repo = MockRecipientRepo::new();
1047 let recip_clone = recipient.clone();
1048 recip_repo
1049 .expect_find_by_id()
1050 .returning(move |_| Ok(Some(recip_clone.clone())));
1051 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1052
1053 let uc = make_use_cases(
1054 MockConvRepo::new(),
1055 recip_repo,
1056 MockOwnerRepo::new(),
1057 MockBuildingRepo::new(),
1058 MockMeetingRepo::new(),
1059 );
1060
1061 let result = uc.set_recipient_proxy(recipient_id, proxy_owner_id).await;
1062
1063 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1064 let resp = result.unwrap();
1065 assert_eq!(resp.proxy_owner_id, Some(proxy_owner_id));
1066 }
1067
1068 #[tokio::test]
1072 async fn test_set_proxy_to_self_error() {
1073 let conv_id = Uuid::new_v4();
1074 let owner_id = Uuid::new_v4();
1075 let recipient = make_recipient(conv_id, owner_id);
1076 let recipient_id = recipient.id;
1077 let self_owner_id = recipient.owner_id;
1078
1079 let mut recip_repo = MockRecipientRepo::new();
1080 let recip_clone = recipient.clone();
1081 recip_repo
1082 .expect_find_by_id()
1083 .returning(move |_| Ok(Some(recip_clone.clone())));
1084
1085 let uc = make_use_cases(
1086 MockConvRepo::new(),
1087 recip_repo,
1088 MockOwnerRepo::new(),
1089 MockBuildingRepo::new(),
1090 MockMeetingRepo::new(),
1091 );
1092
1093 let result = uc.set_recipient_proxy(recipient_id, self_owner_id).await;
1094
1095 assert!(result.is_err());
1096 assert!(result.unwrap_err().contains("Cannot delegate to self"));
1097 }
1098}