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, MeetingType, 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 }
570 }
571
572 mock! {
573 BuildingRepo {}
574
575 #[async_trait]
576 impl BuildingRepository for BuildingRepo {
577 async fn create(&self, building: &Building) -> Result<Building, String>;
578 async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String>;
579 async fn find_all(&self) -> Result<Vec<Building>, String>;
580 async fn find_all_paginated(&self, page_request: &PageRequest, filters: &BuildingFilters) -> Result<(Vec<Building>, i64), String>;
581 async fn update(&self, building: &Building) -> Result<Building, String>;
582 async fn delete(&self, id: Uuid) -> Result<bool, String>;
583 async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String>;
584 }
585 }
586
587 mock! {
588 MeetingRepo {}
589
590 #[async_trait]
591 impl MeetingRepository for MeetingRepo {
592 async fn create(&self, meeting: &Meeting) -> Result<Meeting, String>;
593 async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String>;
594 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String>;
595 async fn update(&self, meeting: &Meeting) -> Result<Meeting, String>;
596 async fn delete(&self, id: Uuid) -> Result<bool, String>;
597 async fn find_all_paginated(&self, page_request: &PageRequest, organization_id: Option<Uuid>) -> Result<(Vec<Meeting>, i64), String>;
598 }
599 }
600
601 fn make_use_cases(
607 conv_repo: MockConvRepo,
608 recip_repo: MockRecipientRepo,
609 owner_repo: MockOwnerRepo,
610 building_repo: MockBuildingRepo,
611 meeting_repo: MockMeetingRepo,
612 ) -> ConvocationUseCases {
613 ConvocationUseCases::new(
614 Arc::new(conv_repo),
615 Arc::new(recip_repo),
616 Arc::new(owner_repo),
617 Arc::new(building_repo),
618 Arc::new(meeting_repo),
619 )
620 }
621
622 fn make_convocation(org_id: Uuid, building_id: Uuid, meeting_id: Uuid) -> Convocation {
624 let meeting_date = Utc::now() + Duration::days(20);
625 Convocation::new(
626 org_id,
627 building_id,
628 meeting_id,
629 ConvocationType::Ordinary,
630 meeting_date,
631 "FR".to_string(),
632 Uuid::new_v4(),
633 )
634 .expect("helper should produce a valid convocation")
635 }
636
637 fn make_sent_convocation(org_id: Uuid, building_id: Uuid, meeting_id: Uuid) -> Convocation {
639 let mut conv = make_convocation(org_id, building_id, meeting_id);
640 conv.mark_sent("/tmp/conv.pdf".to_string(), 5).unwrap();
641 conv
642 }
643
644 fn make_recipient(convocation_id: Uuid, owner_id: Uuid) -> ConvocationRecipient {
646 let mut r =
647 ConvocationRecipient::new(convocation_id, owner_id, "owner@example.com".to_string())
648 .unwrap();
649 r.mark_email_sent();
650 r
651 }
652
653 #[tokio::test]
657 async fn test_create_convocation_ordinary_valid_deadline() {
658 let org_id = Uuid::new_v4();
659 let building_id = Uuid::new_v4();
660 let meeting_id = Uuid::new_v4();
661 let meeting_date = Utc::now() + Duration::days(20);
662
663 let mut conv_repo = MockConvRepo::new();
664 conv_repo.expect_create().returning(|conv| Ok(conv.clone()));
665
666 let uc = make_use_cases(
667 conv_repo,
668 MockRecipientRepo::new(),
669 MockOwnerRepo::new(),
670 MockBuildingRepo::new(),
671 MockMeetingRepo::new(),
672 );
673
674 let request = CreateConvocationRequest {
675 building_id,
676 meeting_id,
677 meeting_type: ConvocationType::Ordinary,
678 meeting_date,
679 language: "FR".to_string(),
680 };
681
682 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
683
684 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
685 let resp = result.unwrap();
686 assert_eq!(resp.status, ConvocationStatus::Draft);
687 assert_eq!(resp.language, "FR");
688 assert!(resp.respects_legal_deadline);
689 }
690
691 #[tokio::test]
696 async fn test_create_convocation_violating_legal_deadline() {
697 let org_id = Uuid::new_v4();
698 let meeting_date = Utc::now() + Duration::days(5); let uc = make_use_cases(
701 MockConvRepo::new(),
702 MockRecipientRepo::new(),
703 MockOwnerRepo::new(),
704 MockBuildingRepo::new(),
705 MockMeetingRepo::new(),
706 );
707
708 let request = CreateConvocationRequest {
709 building_id: Uuid::new_v4(),
710 meeting_id: Uuid::new_v4(),
711 meeting_type: ConvocationType::Ordinary,
712 meeting_date,
713 language: "FR".to_string(),
714 };
715
716 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
717
718 assert!(result.is_err());
719 let err = result.unwrap_err();
720 assert!(
721 err.contains("Meeting date too soon"),
722 "Expected 'Meeting date too soon' error, got: {}",
723 err
724 );
725 }
726
727 #[tokio::test]
732 async fn test_create_convocation_extraordinary_valid_deadline() {
733 let org_id = Uuid::new_v4();
734 let meeting_date = Utc::now() + Duration::days(16); let mut conv_repo = MockConvRepo::new();
737 conv_repo.expect_create().returning(|conv| Ok(conv.clone()));
738
739 let uc = make_use_cases(
740 conv_repo,
741 MockRecipientRepo::new(),
742 MockOwnerRepo::new(),
743 MockBuildingRepo::new(),
744 MockMeetingRepo::new(),
745 );
746
747 let request = CreateConvocationRequest {
748 building_id: Uuid::new_v4(),
749 meeting_id: Uuid::new_v4(),
750 meeting_type: ConvocationType::Extraordinary,
751 meeting_date,
752 language: "NL".to_string(),
753 };
754
755 let result = uc.create_convocation(org_id, request, Uuid::new_v4()).await;
756
757 assert!(result.is_ok());
758 let resp = result.unwrap();
759 assert_eq!(resp.language, "NL");
760 }
761
762 #[tokio::test]
766 async fn test_schedule_convocation_success() {
767 let org_id = Uuid::new_v4();
768 let building_id = Uuid::new_v4();
769 let meeting_id = Uuid::new_v4();
770 let conv = make_convocation(org_id, building_id, meeting_id);
771 let conv_id = conv.id;
772 let min_send_date = conv.minimum_send_date;
773
774 let mut conv_repo = MockConvRepo::new();
775 let conv_clone = conv.clone();
776 conv_repo
777 .expect_find_by_id()
778 .returning(move |_| Ok(Some(conv_clone.clone())));
779 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
780
781 let uc = make_use_cases(
782 conv_repo,
783 MockRecipientRepo::new(),
784 MockOwnerRepo::new(),
785 MockBuildingRepo::new(),
786 MockMeetingRepo::new(),
787 );
788
789 let send_date = min_send_date - Duration::days(1);
791 let request = ScheduleConvocationRequest { send_date };
792
793 let result = uc.schedule_convocation(conv_id, request).await;
794
795 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
796 let resp = result.unwrap();
797 assert_eq!(resp.status, ConvocationStatus::Scheduled);
798 assert!(resp.scheduled_send_date.is_some());
799 }
800
801 #[tokio::test]
805 async fn test_cancel_convocation_success() {
806 let org_id = Uuid::new_v4();
807 let building_id = Uuid::new_v4();
808 let meeting_id = Uuid::new_v4();
809 let conv = make_convocation(org_id, building_id, meeting_id);
810 let conv_id = conv.id;
811
812 let mut conv_repo = MockConvRepo::new();
813 let conv_clone = conv.clone();
814 conv_repo
815 .expect_find_by_id()
816 .returning(move |_| Ok(Some(conv_clone.clone())));
817 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
818
819 let uc = make_use_cases(
820 conv_repo,
821 MockRecipientRepo::new(),
822 MockOwnerRepo::new(),
823 MockBuildingRepo::new(),
824 MockMeetingRepo::new(),
825 );
826
827 let result = uc.cancel_convocation(conv_id).await;
828
829 assert!(result.is_ok());
830 let resp = result.unwrap();
831 assert_eq!(resp.status, ConvocationStatus::Cancelled);
832 }
833
834 #[tokio::test]
838 async fn test_cancel_convocation_already_cancelled_error() {
839 let org_id = Uuid::new_v4();
840 let building_id = Uuid::new_v4();
841 let meeting_id = Uuid::new_v4();
842 let mut conv = make_convocation(org_id, building_id, meeting_id);
843 conv.cancel().unwrap(); let conv_id = conv.id;
845
846 let mut conv_repo = MockConvRepo::new();
847 let conv_clone = conv.clone();
848 conv_repo
849 .expect_find_by_id()
850 .returning(move |_| Ok(Some(conv_clone.clone())));
851
852 let uc = make_use_cases(
853 conv_repo,
854 MockRecipientRepo::new(),
855 MockOwnerRepo::new(),
856 MockBuildingRepo::new(),
857 MockMeetingRepo::new(),
858 );
859
860 let result = uc.cancel_convocation(conv_id).await;
861
862 assert!(result.is_err());
863 assert!(result.unwrap_err().contains("already cancelled"));
864 }
865
866 #[tokio::test]
870 async fn test_send_reminders_marks_recipients() {
871 let org_id = Uuid::new_v4();
872 let building_id = Uuid::new_v4();
873 let meeting_id = Uuid::new_v4();
874 let conv = make_sent_convocation(org_id, building_id, meeting_id);
875 let conv_id = conv.id;
876
877 let owner1_id = Uuid::new_v4();
878 let owner2_id = Uuid::new_v4();
879 let r1 = make_recipient(conv_id, owner1_id);
880 let r2 = make_recipient(conv_id, owner2_id);
881
882 let mut recip_repo = MockRecipientRepo::new();
883 let r1_clone = r1.clone();
884 let r2_clone = r2.clone();
885 recip_repo
886 .expect_find_needing_reminder()
887 .returning(move |_| Ok(vec![r1_clone.clone(), r2_clone.clone()]));
888 recip_repo.expect_update().returning(|r| Ok(r.clone()));
889
890 let mut conv_repo = MockConvRepo::new();
891 let conv_clone = conv.clone();
892 conv_repo
893 .expect_find_by_id()
894 .returning(move |_| Ok(Some(conv_clone.clone())));
895 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
896
897 let uc = make_use_cases(
898 conv_repo,
899 recip_repo,
900 MockOwnerRepo::new(),
901 MockBuildingRepo::new(),
902 MockMeetingRepo::new(),
903 );
904
905 let result = uc.send_reminders(conv_id).await;
906
907 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
908 let recipients = result.unwrap();
909 assert_eq!(recipients.len(), 2);
910 assert!(recipients[0].reminder_sent_at.is_some());
912 assert!(recipients[1].reminder_sent_at.is_some());
913 }
914
915 #[tokio::test]
919 async fn test_mark_email_opened_updates_tracking() {
920 let org_id = Uuid::new_v4();
921 let building_id = Uuid::new_v4();
922 let meeting_id = Uuid::new_v4();
923 let conv = make_sent_convocation(org_id, building_id, meeting_id);
924 let conv_id = conv.id;
925
926 let owner_id = Uuid::new_v4();
927 let recipient = make_recipient(conv_id, owner_id);
928 let recipient_id = recipient.id;
929
930 let mut recip_repo = MockRecipientRepo::new();
931 let recip_clone = recipient.clone();
932 recip_repo
933 .expect_find_by_id()
934 .returning(move |_| Ok(Some(recip_clone.clone())));
935 recip_repo.expect_update().returning(|r| Ok(r.clone()));
936 recip_repo
937 .expect_get_tracking_summary()
938 .returning(move |_| {
939 Ok(RecipientTrackingSummary {
940 total_count: 5,
941 opened_count: 3,
942 will_attend_count: 2,
943 will_not_attend_count: 1,
944 attended_count: 0,
945 did_not_attend_count: 0,
946 pending_count: 2,
947 failed_email_count: 0,
948 })
949 });
950
951 let mut conv_repo = MockConvRepo::new();
952 let conv_clone = conv.clone();
953 conv_repo
954 .expect_find_by_id()
955 .returning(move |_| Ok(Some(conv_clone.clone())));
956 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
957
958 let uc = make_use_cases(
959 conv_repo,
960 recip_repo,
961 MockOwnerRepo::new(),
962 MockBuildingRepo::new(),
963 MockMeetingRepo::new(),
964 );
965
966 let result = uc.mark_recipient_email_opened(recipient_id).await;
967
968 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
969 let resp = result.unwrap();
970 assert!(resp.has_opened_email);
971 }
972
973 #[tokio::test]
977 async fn test_update_attendance_will_attend() {
978 let org_id = Uuid::new_v4();
979 let building_id = Uuid::new_v4();
980 let meeting_id = Uuid::new_v4();
981 let conv = make_sent_convocation(org_id, building_id, meeting_id);
982 let conv_id = conv.id;
983
984 let owner_id = Uuid::new_v4();
985 let recipient = make_recipient(conv_id, owner_id);
986 let recipient_id = recipient.id;
987
988 let mut recip_repo = MockRecipientRepo::new();
989 let recip_clone = recipient.clone();
990 recip_repo
991 .expect_find_by_id()
992 .returning(move |_| Ok(Some(recip_clone.clone())));
993 recip_repo.expect_update().returning(|r| Ok(r.clone()));
994 recip_repo
995 .expect_get_tracking_summary()
996 .returning(move |_| {
997 Ok(RecipientTrackingSummary {
998 total_count: 5,
999 opened_count: 1,
1000 will_attend_count: 1,
1001 will_not_attend_count: 0,
1002 attended_count: 0,
1003 did_not_attend_count: 0,
1004 pending_count: 4,
1005 failed_email_count: 0,
1006 })
1007 });
1008
1009 let mut conv_repo = MockConvRepo::new();
1010 let conv_clone = conv.clone();
1011 conv_repo
1012 .expect_find_by_id()
1013 .returning(move |_| Ok(Some(conv_clone.clone())));
1014 conv_repo.expect_update().returning(|conv| Ok(conv.clone()));
1015
1016 let uc = make_use_cases(
1017 conv_repo,
1018 recip_repo,
1019 MockOwnerRepo::new(),
1020 MockBuildingRepo::new(),
1021 MockMeetingRepo::new(),
1022 );
1023
1024 let result = uc
1025 .update_recipient_attendance(recipient_id, AttendanceStatus::WillAttend)
1026 .await;
1027
1028 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1029 let resp = result.unwrap();
1030 assert_eq!(resp.attendance_status, AttendanceStatus::WillAttend);
1031 assert!(resp.has_confirmed_attendance);
1032 }
1033
1034 #[tokio::test]
1038 async fn test_set_proxy_delegation() {
1039 let conv_id = Uuid::new_v4();
1040 let owner_id = Uuid::new_v4();
1041 let proxy_owner_id = Uuid::new_v4();
1042 let recipient = make_recipient(conv_id, owner_id);
1043 let recipient_id = recipient.id;
1044
1045 let mut recip_repo = MockRecipientRepo::new();
1046 let recip_clone = recipient.clone();
1047 recip_repo
1048 .expect_find_by_id()
1049 .returning(move |_| Ok(Some(recip_clone.clone())));
1050 recip_repo.expect_update().returning(|r| Ok(r.clone()));
1051
1052 let uc = make_use_cases(
1053 MockConvRepo::new(),
1054 recip_repo,
1055 MockOwnerRepo::new(),
1056 MockBuildingRepo::new(),
1057 MockMeetingRepo::new(),
1058 );
1059
1060 let result = uc.set_recipient_proxy(recipient_id, proxy_owner_id).await;
1061
1062 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
1063 let resp = result.unwrap();
1064 assert_eq!(resp.proxy_owner_id, Some(proxy_owner_id));
1065 }
1066
1067 #[tokio::test]
1071 async fn test_set_proxy_to_self_error() {
1072 let conv_id = Uuid::new_v4();
1073 let owner_id = Uuid::new_v4();
1074 let recipient = make_recipient(conv_id, owner_id);
1075 let recipient_id = recipient.id;
1076 let self_owner_id = recipient.owner_id;
1077
1078 let mut recip_repo = MockRecipientRepo::new();
1079 let recip_clone = recipient.clone();
1080 recip_repo
1081 .expect_find_by_id()
1082 .returning(move |_| Ok(Some(recip_clone.clone())));
1083
1084 let uc = make_use_cases(
1085 MockConvRepo::new(),
1086 recip_repo,
1087 MockOwnerRepo::new(),
1088 MockBuildingRepo::new(),
1089 MockMeetingRepo::new(),
1090 );
1091
1092 let result = uc.set_recipient_proxy(recipient_id, self_owner_id).await;
1093
1094 assert!(result.is_err());
1095 assert!(result.unwrap_err().contains("Cannot delegate to self"));
1096 }
1097}