koprogo_api/application/use_cases/
notification_use_cases.rs

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    /// Create a new notification
29    pub async fn create_notification(
30        &self,
31        organization_id: Uuid,
32        request: CreateNotificationRequest,
33    ) -> Result<NotificationResponse, String> {
34        // Check if user has enabled this channel for this notification type
35        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        // InApp notifications are immediately "sent" (delivered to user's inbox)
67        if notification.channel == NotificationChannel::InApp {
68            notification.mark_sent();
69        }
70
71        let created = self.notification_repository.create(&notification).await?;
72        Ok(NotificationResponse::from(created))
73    }
74
75    /// Get a notification by ID
76    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    /// List all notifications for a user
84    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    /// List unread in-app notifications for a user
96    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    /// Mark an in-app notification as read
111    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(&notification).await?;
121        Ok(NotificationResponse::from(updated))
122    }
123
124    /// Mark all in-app notifications as read for a user
125    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    /// Delete a notification
132    pub async fn delete_notification(&self, id: Uuid) -> Result<bool, String> {
133        self.notification_repository.delete(id).await
134    }
135
136    /// Get notification statistics for a user
137    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    // ==================== Notification Preferences ====================
174
175    /// Get user's notification preferences
176    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 user has no preferences yet, create defaults
183        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    /// Get user's preference for a specific notification type
201    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                // Create default preference for this type
214                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    /// Update user's notification preference for a specific notification type
222    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                // Create default if doesn't exist
236                let default_pref = NotificationPreference::new(user_id, notification_type);
237                self.preference_repository.create(&default_pref).await?
238            }
239        };
240
241        // Update channels as requested
242        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    // ==================== Admin/System Methods ====================
259
260    /// Get all pending notifications (for background processing)
261    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    /// Get all failed notifications (for retry)
270    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    /// Mark notification as sent
279    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(&notification).await?;
289        Ok(NotificationResponse::from(updated))
290    }
291
292    /// Mark notification as failed
293    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(&notification).await?;
307        Ok(NotificationResponse::from(updated))
308    }
309
310    /// Retry a failed notification
311    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(&notification).await?;
321        Ok(NotificationResponse::from(updated))
322    }
323
324    /// Delete old notifications (cleanup)
325    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    // ==================== Mock Repositories ====================
344
345    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        /// When true, is_channel_enabled always returns true (default behaviour).
537        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    // ==================== Helper ====================
639
640    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    // ==================== Tests ====================
663
664    #[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(), // channel_enabled = true
672        );
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        // InApp notifications should be automatically marked as Sent
693        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        // One unread (Sent InApp)
772        let n_unread = make_in_app_sent_notification(user_id, org_id);
773
774        // One read (Sent InApp then marked read)
775        let mut n_read = make_in_app_sent_notification(user_id, org_id);
776        n_read.mark_read().unwrap();
777
778        // One email notification (should not appear in unread in-app)
779        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()); // true = was deleted
830
831        // Verify it no longer exists
832        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        // 1 sent in-app (unread)
843        let n_sent = make_in_app_sent_notification(user_id, org_id);
844
845        // 1 pending email
846        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(); // status = Pending
856
857        // 1 failed email
858        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); // only the in-app sent notification
883        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        // Seed an existing preference
893        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,     // unchanged
902            push_enabled: Some(true), // unchanged (already true by default)
903        };
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); // disabled
913        assert!(resp.in_app_enabled); // unchanged default
914        assert!(resp.push_enabled); // unchanged
915    }
916}