1use crate::application::dto::{
2 CreateNotificationRequest, NotificationPreferenceResponse, NotificationResponse,
3 NotificationStats, UpdatePreferenceRequest,
4};
5use crate::application::ports::{NotificationPreferenceRepository, NotificationRepository};
6use crate::domain::entities::{
7 Notification, NotificationChannel, NotificationPreference, NotificationStatus, NotificationType,
8};
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub struct NotificationUseCases {
13 notification_repository: Arc<dyn NotificationRepository>,
14 preference_repository: Arc<dyn NotificationPreferenceRepository>,
15}
16
17impl NotificationUseCases {
18 pub fn new(
19 notification_repository: Arc<dyn NotificationRepository>,
20 preference_repository: Arc<dyn NotificationPreferenceRepository>,
21 ) -> Self {
22 Self {
23 notification_repository,
24 preference_repository,
25 }
26 }
27
28 pub async fn create_notification(
30 &self,
31 organization_id: Uuid,
32 request: CreateNotificationRequest,
33 ) -> Result<NotificationResponse, String> {
34 let is_enabled = self
36 .preference_repository
37 .is_channel_enabled(
38 request.user_id,
39 request.notification_type.clone(),
40 request.channel.clone(),
41 )
42 .await?;
43
44 if !is_enabled {
45 return Err("User has disabled this notification channel".to_string());
46 }
47
48 let mut notification = Notification::new(
49 organization_id,
50 request.user_id,
51 request.notification_type,
52 request.channel,
53 request.priority,
54 request.title,
55 request.message,
56 )?;
57
58 if let Some(link_url) = request.link_url {
59 notification = notification.with_link(link_url);
60 }
61
62 if let Some(metadata) = request.metadata {
63 notification = notification.with_metadata(metadata);
64 }
65
66 if notification.channel == NotificationChannel::InApp {
68 notification.mark_sent();
69 }
70
71 let created = self.notification_repository.create(¬ification).await?;
72 Ok(NotificationResponse::from(created))
73 }
74
75 pub async fn get_notification(&self, id: Uuid) -> Result<Option<NotificationResponse>, String> {
77 match self.notification_repository.find_by_id(id).await? {
78 Some(notification) => Ok(Some(NotificationResponse::from(notification))),
79 None => Ok(None),
80 }
81 }
82
83 pub async fn list_user_notifications(
85 &self,
86 user_id: Uuid,
87 ) -> Result<Vec<NotificationResponse>, String> {
88 let notifications = self.notification_repository.find_by_user(user_id).await?;
89 Ok(notifications
90 .into_iter()
91 .map(NotificationResponse::from)
92 .collect())
93 }
94
95 pub async fn list_unread_notifications(
97 &self,
98 user_id: Uuid,
99 ) -> Result<Vec<NotificationResponse>, String> {
100 let notifications = self
101 .notification_repository
102 .find_unread_by_user(user_id)
103 .await?;
104 Ok(notifications
105 .into_iter()
106 .map(NotificationResponse::from)
107 .collect())
108 }
109
110 pub async fn mark_as_read(&self, id: Uuid) -> Result<NotificationResponse, String> {
112 let mut notification = self
113 .notification_repository
114 .find_by_id(id)
115 .await?
116 .ok_or_else(|| "Notification not found".to_string())?;
117
118 notification.mark_read()?;
119
120 let updated = self.notification_repository.update(¬ification).await?;
121 Ok(NotificationResponse::from(updated))
122 }
123
124 pub async fn mark_all_read(&self, user_id: Uuid) -> Result<i64, String> {
126 self.notification_repository
127 .mark_all_read_by_user(user_id)
128 .await
129 }
130
131 pub async fn delete_notification(&self, id: Uuid) -> Result<bool, String> {
133 self.notification_repository.delete(id).await
134 }
135
136 pub async fn get_user_stats(&self, user_id: Uuid) -> Result<NotificationStats, String> {
138 let total = self
139 .notification_repository
140 .find_by_user(user_id)
141 .await?
142 .len() as i64;
143
144 let unread = self
145 .notification_repository
146 .count_unread_by_user(user_id)
147 .await?;
148
149 let pending = self
150 .notification_repository
151 .count_by_user_and_status(user_id, NotificationStatus::Pending)
152 .await?;
153
154 let sent = self
155 .notification_repository
156 .count_by_user_and_status(user_id, NotificationStatus::Sent)
157 .await?;
158
159 let failed = self
160 .notification_repository
161 .count_by_user_and_status(user_id, NotificationStatus::Failed)
162 .await?;
163
164 Ok(NotificationStats {
165 total,
166 unread,
167 pending,
168 sent,
169 failed,
170 })
171 }
172
173 pub async fn get_user_preferences(
177 &self,
178 user_id: Uuid,
179 ) -> Result<Vec<NotificationPreferenceResponse>, String> {
180 let preferences = self.preference_repository.find_by_user(user_id).await?;
181
182 if preferences.is_empty() {
184 let defaults = self
185 .preference_repository
186 .create_defaults_for_user(user_id)
187 .await?;
188 return Ok(defaults
189 .into_iter()
190 .map(NotificationPreferenceResponse::from)
191 .collect());
192 }
193
194 Ok(preferences
195 .into_iter()
196 .map(NotificationPreferenceResponse::from)
197 .collect())
198 }
199
200 pub async fn get_preference(
202 &self,
203 user_id: Uuid,
204 notification_type: NotificationType,
205 ) -> Result<Option<NotificationPreferenceResponse>, String> {
206 match self
207 .preference_repository
208 .find_by_user_and_type(user_id, notification_type.clone())
209 .await?
210 {
211 Some(pref) => Ok(Some(NotificationPreferenceResponse::from(pref))),
212 None => {
213 let default_pref = NotificationPreference::new(user_id, notification_type);
215 let created = self.preference_repository.create(&default_pref).await?;
216 Ok(Some(NotificationPreferenceResponse::from(created)))
217 }
218 }
219 }
220
221 pub async fn update_preference(
223 &self,
224 user_id: Uuid,
225 notification_type: NotificationType,
226 request: UpdatePreferenceRequest,
227 ) -> Result<NotificationPreferenceResponse, String> {
228 let mut preference = match self
229 .preference_repository
230 .find_by_user_and_type(user_id, notification_type.clone())
231 .await?
232 {
233 Some(pref) => pref,
234 None => {
235 let default_pref = NotificationPreference::new(user_id, notification_type);
237 self.preference_repository.create(&default_pref).await?
238 }
239 };
240
241 if let Some(email_enabled) = request.email_enabled {
243 preference.set_channel_enabled(NotificationChannel::Email, email_enabled);
244 }
245
246 if let Some(in_app_enabled) = request.in_app_enabled {
247 preference.set_channel_enabled(NotificationChannel::InApp, in_app_enabled);
248 }
249
250 if let Some(push_enabled) = request.push_enabled {
251 preference.set_channel_enabled(NotificationChannel::Push, push_enabled);
252 }
253
254 let updated = self.preference_repository.update(&preference).await?;
255 Ok(NotificationPreferenceResponse::from(updated))
256 }
257
258 pub async fn get_pending_notifications(&self) -> Result<Vec<NotificationResponse>, String> {
262 let notifications = self.notification_repository.find_pending().await?;
263 Ok(notifications
264 .into_iter()
265 .map(NotificationResponse::from)
266 .collect())
267 }
268
269 pub async fn get_failed_notifications(&self) -> Result<Vec<NotificationResponse>, String> {
271 let notifications = self.notification_repository.find_failed().await?;
272 Ok(notifications
273 .into_iter()
274 .map(NotificationResponse::from)
275 .collect())
276 }
277
278 pub async fn mark_as_sent(&self, id: Uuid) -> Result<NotificationResponse, String> {
280 let mut notification = self
281 .notification_repository
282 .find_by_id(id)
283 .await?
284 .ok_or_else(|| "Notification not found".to_string())?;
285
286 notification.mark_sent();
287
288 let updated = self.notification_repository.update(¬ification).await?;
289 Ok(NotificationResponse::from(updated))
290 }
291
292 pub async fn mark_as_failed(
294 &self,
295 id: Uuid,
296 error_message: String,
297 ) -> Result<NotificationResponse, String> {
298 let mut notification = self
299 .notification_repository
300 .find_by_id(id)
301 .await?
302 .ok_or_else(|| "Notification not found".to_string())?;
303
304 notification.mark_failed(error_message);
305
306 let updated = self.notification_repository.update(¬ification).await?;
307 Ok(NotificationResponse::from(updated))
308 }
309
310 pub async fn retry_notification(&self, id: Uuid) -> Result<NotificationResponse, String> {
312 let mut notification = self
313 .notification_repository
314 .find_by_id(id)
315 .await?
316 .ok_or_else(|| "Notification not found".to_string())?;
317
318 notification.retry()?;
319
320 let updated = self.notification_repository.update(¬ification).await?;
321 Ok(NotificationResponse::from(updated))
322 }
323
324 pub async fn cleanup_old_notifications(&self, days: i64) -> Result<i64, String> {
326 self.notification_repository.delete_older_than(days).await
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::application::ports::{NotificationPreferenceRepository, NotificationRepository};
334 use crate::domain::entities::{
335 Notification, NotificationChannel, NotificationPreference, NotificationPriority,
336 NotificationStatus, NotificationType,
337 };
338 use async_trait::async_trait;
339 use chrono::Utc;
340 use std::collections::HashMap;
341 use std::sync::Mutex;
342
343 struct MockNotificationRepository {
346 notifications: Mutex<HashMap<Uuid, Notification>>,
347 }
348
349 impl MockNotificationRepository {
350 fn new() -> Self {
351 Self {
352 notifications: Mutex::new(HashMap::new()),
353 }
354 }
355
356 fn with_notification(self, notification: Notification) -> Self {
357 self.notifications
358 .lock()
359 .unwrap()
360 .insert(notification.id, notification);
361 self
362 }
363 }
364
365 #[async_trait]
366 impl NotificationRepository for MockNotificationRepository {
367 async fn create(&self, notification: &Notification) -> Result<Notification, String> {
368 let n = notification.clone();
369 self.notifications.lock().unwrap().insert(n.id, n.clone());
370 Ok(n)
371 }
372
373 async fn find_by_id(&self, id: Uuid) -> Result<Option<Notification>, String> {
374 Ok(self.notifications.lock().unwrap().get(&id).cloned())
375 }
376
377 async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<Notification>, String> {
378 Ok(self
379 .notifications
380 .lock()
381 .unwrap()
382 .values()
383 .filter(|n| n.user_id == user_id)
384 .cloned()
385 .collect())
386 }
387
388 async fn find_by_user_and_status(
389 &self,
390 user_id: Uuid,
391 status: NotificationStatus,
392 ) -> Result<Vec<Notification>, String> {
393 Ok(self
394 .notifications
395 .lock()
396 .unwrap()
397 .values()
398 .filter(|n| n.user_id == user_id && n.status == status)
399 .cloned()
400 .collect())
401 }
402
403 async fn find_by_user_and_channel(
404 &self,
405 user_id: Uuid,
406 channel: NotificationChannel,
407 ) -> Result<Vec<Notification>, String> {
408 Ok(self
409 .notifications
410 .lock()
411 .unwrap()
412 .values()
413 .filter(|n| n.user_id == user_id && n.channel == channel)
414 .cloned()
415 .collect())
416 }
417
418 async fn find_unread_by_user(&self, user_id: Uuid) -> Result<Vec<Notification>, String> {
419 Ok(self
420 .notifications
421 .lock()
422 .unwrap()
423 .values()
424 .filter(|n| {
425 n.user_id == user_id
426 && n.channel == NotificationChannel::InApp
427 && n.status == NotificationStatus::Sent
428 && n.read_at.is_none()
429 })
430 .cloned()
431 .collect())
432 }
433
434 async fn find_pending(&self) -> Result<Vec<Notification>, String> {
435 Ok(self
436 .notifications
437 .lock()
438 .unwrap()
439 .values()
440 .filter(|n| n.status == NotificationStatus::Pending)
441 .cloned()
442 .collect())
443 }
444
445 async fn find_failed(&self) -> Result<Vec<Notification>, String> {
446 Ok(self
447 .notifications
448 .lock()
449 .unwrap()
450 .values()
451 .filter(|n| n.status == NotificationStatus::Failed)
452 .cloned()
453 .collect())
454 }
455
456 async fn find_by_organization(
457 &self,
458 organization_id: Uuid,
459 ) -> Result<Vec<Notification>, String> {
460 Ok(self
461 .notifications
462 .lock()
463 .unwrap()
464 .values()
465 .filter(|n| n.organization_id == organization_id)
466 .cloned()
467 .collect())
468 }
469
470 async fn update(&self, notification: &Notification) -> Result<Notification, String> {
471 self.notifications
472 .lock()
473 .unwrap()
474 .insert(notification.id, notification.clone());
475 Ok(notification.clone())
476 }
477
478 async fn delete(&self, id: Uuid) -> Result<bool, String> {
479 Ok(self.notifications.lock().unwrap().remove(&id).is_some())
480 }
481
482 async fn count_unread_by_user(&self, user_id: Uuid) -> Result<i64, String> {
483 let count = self
484 .notifications
485 .lock()
486 .unwrap()
487 .values()
488 .filter(|n| {
489 n.user_id == user_id
490 && n.channel == NotificationChannel::InApp
491 && n.status == NotificationStatus::Sent
492 && n.read_at.is_none()
493 })
494 .count();
495 Ok(count as i64)
496 }
497
498 async fn count_by_user_and_status(
499 &self,
500 user_id: Uuid,
501 status: NotificationStatus,
502 ) -> Result<i64, String> {
503 let count = self
504 .notifications
505 .lock()
506 .unwrap()
507 .values()
508 .filter(|n| n.user_id == user_id && n.status == status)
509 .count();
510 Ok(count as i64)
511 }
512
513 async fn mark_all_read_by_user(&self, user_id: Uuid) -> Result<i64, String> {
514 let mut store = self.notifications.lock().unwrap();
515 let mut count = 0i64;
516 for n in store.values_mut() {
517 if n.user_id == user_id
518 && n.channel == NotificationChannel::InApp
519 && n.status == NotificationStatus::Sent
520 {
521 n.status = NotificationStatus::Read;
522 n.read_at = Some(Utc::now());
523 count += 1;
524 }
525 }
526 Ok(count)
527 }
528
529 async fn delete_older_than(&self, _days: i64) -> Result<i64, String> {
530 Ok(0)
531 }
532 }
533
534 struct MockNotificationPreferenceRepository {
535 preferences: Mutex<HashMap<Uuid, NotificationPreference>>,
536 channel_enabled: Mutex<bool>,
538 }
539
540 impl MockNotificationPreferenceRepository {
541 fn new() -> Self {
542 Self {
543 preferences: Mutex::new(HashMap::new()),
544 channel_enabled: Mutex::new(true),
545 }
546 }
547
548 fn with_channel_disabled(self) -> Self {
549 *self.channel_enabled.lock().unwrap() = false;
550 self
551 }
552
553 fn with_preference(self, pref: NotificationPreference) -> Self {
554 self.preferences.lock().unwrap().insert(pref.id, pref);
555 self
556 }
557 }
558
559 #[async_trait]
560 impl NotificationPreferenceRepository for MockNotificationPreferenceRepository {
561 async fn create(
562 &self,
563 preference: &NotificationPreference,
564 ) -> Result<NotificationPreference, String> {
565 self.preferences
566 .lock()
567 .unwrap()
568 .insert(preference.id, preference.clone());
569 Ok(preference.clone())
570 }
571
572 async fn find_by_id(&self, id: Uuid) -> Result<Option<NotificationPreference>, String> {
573 Ok(self.preferences.lock().unwrap().get(&id).cloned())
574 }
575
576 async fn find_by_user_and_type(
577 &self,
578 user_id: Uuid,
579 notification_type: NotificationType,
580 ) -> Result<Option<NotificationPreference>, String> {
581 Ok(self
582 .preferences
583 .lock()
584 .unwrap()
585 .values()
586 .find(|p| p.user_id == user_id && p.notification_type == notification_type)
587 .cloned())
588 }
589
590 async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<NotificationPreference>, String> {
591 Ok(self
592 .preferences
593 .lock()
594 .unwrap()
595 .values()
596 .filter(|p| p.user_id == user_id)
597 .cloned()
598 .collect())
599 }
600
601 async fn update(
602 &self,
603 preference: &NotificationPreference,
604 ) -> Result<NotificationPreference, String> {
605 self.preferences
606 .lock()
607 .unwrap()
608 .insert(preference.id, preference.clone());
609 Ok(preference.clone())
610 }
611
612 async fn delete(&self, id: Uuid) -> Result<bool, String> {
613 Ok(self.preferences.lock().unwrap().remove(&id).is_some())
614 }
615
616 async fn is_channel_enabled(
617 &self,
618 _user_id: Uuid,
619 _notification_type: NotificationType,
620 _channel: NotificationChannel,
621 ) -> Result<bool, String> {
622 Ok(*self.channel_enabled.lock().unwrap())
623 }
624
625 async fn create_defaults_for_user(
626 &self,
627 user_id: Uuid,
628 ) -> Result<Vec<NotificationPreference>, String> {
629 let pref = NotificationPreference::new(user_id, NotificationType::System);
630 self.preferences
631 .lock()
632 .unwrap()
633 .insert(pref.id, pref.clone());
634 Ok(vec![pref])
635 }
636 }
637
638 fn make_use_cases(
641 notif_repo: MockNotificationRepository,
642 pref_repo: MockNotificationPreferenceRepository,
643 ) -> NotificationUseCases {
644 NotificationUseCases::new(Arc::new(notif_repo), Arc::new(pref_repo))
645 }
646
647 fn make_in_app_sent_notification(user_id: Uuid, org_id: Uuid) -> Notification {
648 let mut n = Notification::new(
649 org_id,
650 user_id,
651 NotificationType::TicketResolved,
652 NotificationChannel::InApp,
653 NotificationPriority::Medium,
654 "Ticket resolved".to_string(),
655 "Your ticket has been resolved.".to_string(),
656 )
657 .unwrap();
658 n.mark_sent();
659 n
660 }
661
662 #[tokio::test]
665 async fn test_create_notification_success() {
666 let org_id = Uuid::new_v4();
667 let user_id = Uuid::new_v4();
668
669 let uc = make_use_cases(
670 MockNotificationRepository::new(),
671 MockNotificationPreferenceRepository::new(), );
673
674 let request = CreateNotificationRequest {
675 user_id,
676 notification_type: NotificationType::ExpenseCreated,
677 channel: NotificationChannel::InApp,
678 priority: NotificationPriority::High,
679 title: "Nouvel appel de fonds".to_string(),
680 message: "Un appel de 500 EUR a ete cree.".to_string(),
681 link_url: Some("https://app.koprogo.be/expenses/123".to_string()),
682 metadata: None,
683 };
684
685 let result = uc.create_notification(org_id, request).await;
686 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
687
688 let resp = result.unwrap();
689 assert_eq!(resp.organization_id, org_id);
690 assert_eq!(resp.user_id, user_id);
691 assert_eq!(resp.title, "Nouvel appel de fonds");
692 assert_eq!(resp.status, NotificationStatus::Sent);
694 assert!(resp.sent_at.is_some());
695 assert!(resp.link_url.is_some());
696 }
697
698 #[tokio::test]
699 async fn test_create_notification_channel_disabled() {
700 let org_id = Uuid::new_v4();
701 let user_id = Uuid::new_v4();
702
703 let uc = make_use_cases(
704 MockNotificationRepository::new(),
705 MockNotificationPreferenceRepository::new().with_channel_disabled(),
706 );
707
708 let request = CreateNotificationRequest {
709 user_id,
710 notification_type: NotificationType::PaymentReminder,
711 channel: NotificationChannel::Email,
712 priority: NotificationPriority::Medium,
713 title: "Relance paiement".to_string(),
714 message: "Votre paiement est en retard.".to_string(),
715 link_url: None,
716 metadata: None,
717 };
718
719 let result = uc.create_notification(org_id, request).await;
720 assert!(result.is_err());
721 assert_eq!(
722 result.unwrap_err(),
723 "User has disabled this notification channel"
724 );
725 }
726
727 #[tokio::test]
728 async fn test_mark_as_read() {
729 let org_id = Uuid::new_v4();
730 let user_id = Uuid::new_v4();
731 let notification = make_in_app_sent_notification(user_id, org_id);
732 let notif_id = notification.id;
733
734 let uc = make_use_cases(
735 MockNotificationRepository::new().with_notification(notification),
736 MockNotificationPreferenceRepository::new(),
737 );
738
739 let result = uc.mark_as_read(notif_id).await;
740 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
741
742 let resp = result.unwrap();
743 assert_eq!(resp.status, NotificationStatus::Read);
744 assert!(resp.read_at.is_some());
745 }
746
747 #[tokio::test]
748 async fn test_mark_all_read() {
749 let org_id = Uuid::new_v4();
750 let user_id = Uuid::new_v4();
751
752 let n1 = make_in_app_sent_notification(user_id, org_id);
753 let n2 = make_in_app_sent_notification(user_id, org_id);
754
755 let repo = MockNotificationRepository::new()
756 .with_notification(n1)
757 .with_notification(n2);
758
759 let uc = make_use_cases(repo, MockNotificationPreferenceRepository::new());
760
761 let result = uc.mark_all_read(user_id).await;
762 assert!(result.is_ok());
763 assert_eq!(result.unwrap(), 2);
764 }
765
766 #[tokio::test]
767 async fn test_list_unread_notifications() {
768 let org_id = Uuid::new_v4();
769 let user_id = Uuid::new_v4();
770
771 let n_unread = make_in_app_sent_notification(user_id, org_id);
773
774 let mut n_read = make_in_app_sent_notification(user_id, org_id);
776 n_read.mark_read().unwrap();
777
778 let n_email = Notification::new(
780 org_id,
781 user_id,
782 NotificationType::PaymentReceived,
783 NotificationChannel::Email,
784 NotificationPriority::Low,
785 "Paiement recu".to_string(),
786 "Votre paiement a ete recu.".to_string(),
787 )
788 .unwrap();
789
790 let repo = MockNotificationRepository::new()
791 .with_notification(n_unread)
792 .with_notification(n_read)
793 .with_notification(n_email);
794
795 let uc = make_use_cases(repo, MockNotificationPreferenceRepository::new());
796
797 let result = uc.list_unread_notifications(user_id).await;
798 assert!(result.is_ok());
799 let unread = result.unwrap();
800 assert_eq!(unread.len(), 1);
801 assert_eq!(unread[0].status, NotificationStatus::Sent);
802 assert_eq!(unread[0].channel, NotificationChannel::InApp);
803 }
804
805 #[tokio::test]
806 async fn test_delete_notification() {
807 let org_id = Uuid::new_v4();
808 let user_id = Uuid::new_v4();
809
810 let notification = Notification::new(
811 org_id,
812 user_id,
813 NotificationType::System,
814 NotificationChannel::InApp,
815 NotificationPriority::Low,
816 "System update".to_string(),
817 "The system will be under maintenance.".to_string(),
818 )
819 .unwrap();
820 let notif_id = notification.id;
821
822 let uc = make_use_cases(
823 MockNotificationRepository::new().with_notification(notification),
824 MockNotificationPreferenceRepository::new(),
825 );
826
827 let result = uc.delete_notification(notif_id).await;
828 assert!(result.is_ok());
829 assert!(result.unwrap()); let get_result = uc.get_notification(notif_id).await;
833 assert!(get_result.is_ok());
834 assert!(get_result.unwrap().is_none());
835 }
836
837 #[tokio::test]
838 async fn test_get_user_stats() {
839 let org_id = Uuid::new_v4();
840 let user_id = Uuid::new_v4();
841
842 let n_sent = make_in_app_sent_notification(user_id, org_id);
844
845 let n_pending = Notification::new(
847 org_id,
848 user_id,
849 NotificationType::MeetingConvocation,
850 NotificationChannel::Email,
851 NotificationPriority::High,
852 "Convocation AG".to_string(),
853 "Vous etes convoque a l'AG.".to_string(),
854 )
855 .unwrap(); let mut n_failed = Notification::new(
859 org_id,
860 user_id,
861 NotificationType::PaymentReminder,
862 NotificationChannel::Email,
863 NotificationPriority::Medium,
864 "Relance".to_string(),
865 "Paiement en retard.".to_string(),
866 )
867 .unwrap();
868 n_failed.mark_failed("SMTP error".to_string());
869
870 let repo = MockNotificationRepository::new()
871 .with_notification(n_sent)
872 .with_notification(n_pending)
873 .with_notification(n_failed);
874
875 let uc = make_use_cases(repo, MockNotificationPreferenceRepository::new());
876
877 let result = uc.get_user_stats(user_id).await;
878 assert!(result.is_ok());
879
880 let stats = result.unwrap();
881 assert_eq!(stats.total, 3);
882 assert_eq!(stats.unread, 1); assert_eq!(stats.pending, 1);
884 assert_eq!(stats.sent, 1);
885 assert_eq!(stats.failed, 1);
886 }
887
888 #[tokio::test]
889 async fn test_update_preference() {
890 let user_id = Uuid::new_v4();
891
892 let pref = NotificationPreference::new(user_id, NotificationType::ExpenseCreated);
894
895 let pref_repo = MockNotificationPreferenceRepository::new().with_preference(pref);
896
897 let uc = make_use_cases(MockNotificationRepository::new(), pref_repo);
898
899 let request = UpdatePreferenceRequest {
900 email_enabled: Some(false),
901 in_app_enabled: None, push_enabled: Some(true), };
904
905 let result = uc
906 .update_preference(user_id, NotificationType::ExpenseCreated, request)
907 .await;
908 assert!(result.is_ok(), "Expected Ok, got: {:?}", result.err());
909
910 let resp = result.unwrap();
911 assert_eq!(resp.user_id, user_id);
912 assert!(!resp.email_enabled); assert!(resp.in_app_enabled); assert!(resp.push_enabled); }
916}