1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7pub enum NotificationType {
8 ExpenseCreated, MeetingConvocation, PaymentReceived, TicketResolved, DocumentAdded, BoardMessage, PaymentReminder, BudgetApproved, ResolutionVote, System, }
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub enum NotificationChannel {
23 Email, InApp, Push, }
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30pub enum NotificationStatus {
31 Pending, Sent, Failed, Read, }
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd)]
39pub enum NotificationPriority {
40 Low, Medium, High, Critical, }
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct Notification {
52 pub id: Uuid,
53 pub organization_id: Uuid,
54 pub user_id: Uuid, pub notification_type: NotificationType,
56 pub channel: NotificationChannel,
57 pub priority: NotificationPriority,
58 pub status: NotificationStatus,
59 pub title: String,
60 pub message: String,
61 pub link_url: Option<String>, pub metadata: Option<String>, pub sent_at: Option<DateTime<Utc>>,
64 pub read_at: Option<DateTime<Utc>>, pub created_at: DateTime<Utc>,
66 pub error_message: Option<String>, }
68
69impl Notification {
70 pub fn new(
72 organization_id: Uuid,
73 user_id: Uuid,
74 notification_type: NotificationType,
75 channel: NotificationChannel,
76 priority: NotificationPriority,
77 title: String,
78 message: String,
79 ) -> Result<Self, String> {
80 if title.trim().is_empty() {
82 return Err("Title cannot be empty".to_string());
83 }
84
85 if title.len() > 200 {
86 return Err("Title cannot exceed 200 characters".to_string());
87 }
88
89 if message.trim().is_empty() {
90 return Err("Message cannot be empty".to_string());
91 }
92
93 if message.len() > 5000 {
94 return Err("Message cannot exceed 5000 characters".to_string());
95 }
96
97 let now = Utc::now();
98
99 Ok(Self {
100 id: Uuid::new_v4(),
101 organization_id,
102 user_id,
103 notification_type,
104 channel,
105 priority,
106 status: NotificationStatus::Pending,
107 title,
108 message,
109 link_url: None,
110 metadata: None,
111 sent_at: None,
112 read_at: None,
113 created_at: now,
114 error_message: None,
115 })
116 }
117
118 pub fn with_link(mut self, url: String) -> Self {
120 self.link_url = Some(url);
121 self
122 }
123
124 pub fn with_metadata(mut self, metadata: String) -> Self {
126 self.metadata = Some(metadata);
127 self
128 }
129
130 pub fn mark_sent(&mut self) {
132 self.status = NotificationStatus::Sent;
133 self.sent_at = Some(Utc::now());
134 self.error_message = None;
135 }
136
137 pub fn mark_failed(&mut self, error: String) {
139 self.status = NotificationStatus::Failed;
140 self.error_message = Some(error);
141 }
142
143 pub fn mark_read(&mut self) -> Result<(), String> {
145 if self.channel != NotificationChannel::InApp {
146 return Err("Only in-app notifications can be marked as read".to_string());
147 }
148
149 if self.status != NotificationStatus::Sent {
150 return Err("Can only mark sent notifications as read".to_string());
151 }
152
153 self.status = NotificationStatus::Read;
154 self.read_at = Some(Utc::now());
155 Ok(())
156 }
157
158 pub fn is_unread(&self) -> bool {
160 self.channel == NotificationChannel::InApp
161 && self.status == NotificationStatus::Sent
162 && self.read_at.is_none()
163 }
164
165 pub fn is_pending(&self) -> bool {
167 self.status == NotificationStatus::Pending
168 }
169
170 pub fn retry(&mut self) -> Result<(), String> {
172 if self.status != NotificationStatus::Failed {
173 return Err("Can only retry failed notifications".to_string());
174 }
175
176 self.status = NotificationStatus::Pending;
177 self.error_message = None;
178 Ok(())
179 }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct NotificationPreference {
187 pub id: Uuid,
188 pub user_id: Uuid,
189 pub notification_type: NotificationType,
190 pub email_enabled: bool,
191 pub in_app_enabled: bool,
192 pub push_enabled: bool,
193 pub created_at: DateTime<Utc>,
194 pub updated_at: DateTime<Utc>,
195}
196
197impl NotificationPreference {
198 pub fn new(user_id: Uuid, notification_type: NotificationType) -> Self {
200 let now = Utc::now();
201
202 Self {
203 id: Uuid::new_v4(),
204 user_id,
205 notification_type,
206 email_enabled: true,
208 in_app_enabled: true,
209 push_enabled: true,
210 created_at: now,
211 updated_at: now,
212 }
213 }
214
215 pub fn set_channel_enabled(&mut self, channel: NotificationChannel, enabled: bool) {
217 match channel {
218 NotificationChannel::Email => self.email_enabled = enabled,
219 NotificationChannel::InApp => self.in_app_enabled = enabled,
220 NotificationChannel::Push => self.push_enabled = enabled,
221 }
222 self.updated_at = Utc::now();
223 }
224
225 pub fn is_channel_enabled(&self, channel: &NotificationChannel) -> bool {
227 match channel {
228 NotificationChannel::Email => self.email_enabled,
229 NotificationChannel::InApp => self.in_app_enabled,
230 NotificationChannel::Push => self.push_enabled,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_create_notification_success() {
241 let notification = Notification::new(
242 Uuid::new_v4(),
243 Uuid::new_v4(),
244 NotificationType::ExpenseCreated,
245 NotificationChannel::Email,
246 NotificationPriority::High,
247 "Nouvel appel de fonds".to_string(),
248 "Un nouvel appel de fonds de 500€ a été créé.".to_string(),
249 );
250
251 assert!(notification.is_ok());
252 let notification = notification.unwrap();
253 assert_eq!(notification.status, NotificationStatus::Pending);
254 assert!(notification.sent_at.is_none());
255 }
256
257 #[test]
258 fn test_create_notification_empty_title() {
259 let result = Notification::new(
260 Uuid::new_v4(),
261 Uuid::new_v4(),
262 NotificationType::System,
263 NotificationChannel::InApp,
264 NotificationPriority::Low,
265 " ".to_string(),
266 "Message".to_string(),
267 );
268
269 assert!(result.is_err());
270 assert_eq!(result.unwrap_err(), "Title cannot be empty");
271 }
272
273 #[test]
274 fn test_mark_sent() {
275 let mut notification = Notification::new(
276 Uuid::new_v4(),
277 Uuid::new_v4(),
278 NotificationType::PaymentReceived,
279 NotificationChannel::Email,
280 NotificationPriority::Medium,
281 "Paiement reçu".to_string(),
282 "Votre paiement a été reçu.".to_string(),
283 )
284 .unwrap();
285
286 notification.mark_sent();
287
288 assert_eq!(notification.status, NotificationStatus::Sent);
289 assert!(notification.sent_at.is_some());
290 assert!(notification.error_message.is_none());
291 }
292
293 #[test]
294 fn test_mark_failed() {
295 let mut notification = Notification::new(
296 Uuid::new_v4(),
297 Uuid::new_v4(),
298 NotificationType::TicketResolved,
299 NotificationChannel::Email,
300 NotificationPriority::Low,
301 "Ticket résolu".to_string(),
302 "Votre ticket a été résolu.".to_string(),
303 )
304 .unwrap();
305
306 notification.mark_failed("SMTP error".to_string());
307
308 assert_eq!(notification.status, NotificationStatus::Failed);
309 assert_eq!(notification.error_message, Some("SMTP error".to_string()));
310 }
311
312 #[test]
313 fn test_mark_read_in_app() {
314 let mut notification = Notification::new(
315 Uuid::new_v4(),
316 Uuid::new_v4(),
317 NotificationType::DocumentAdded,
318 NotificationChannel::InApp,
319 NotificationPriority::Low,
320 "Nouveau document".to_string(),
321 "Un nouveau document a été ajouté.".to_string(),
322 )
323 .unwrap();
324
325 notification.mark_sent();
326 let result = notification.mark_read();
327
328 assert!(result.is_ok());
329 assert_eq!(notification.status, NotificationStatus::Read);
330 assert!(notification.read_at.is_some());
331 }
332
333 #[test]
334 fn test_cannot_mark_read_email() {
335 let mut notification = Notification::new(
336 Uuid::new_v4(),
337 Uuid::new_v4(),
338 NotificationType::BoardMessage,
339 NotificationChannel::Email,
340 NotificationPriority::Medium,
341 "Message du conseil".to_string(),
342 "Le conseil a envoyé un message.".to_string(),
343 )
344 .unwrap();
345
346 notification.mark_sent();
347 let result = notification.mark_read();
348
349 assert!(result.is_err());
350 assert!(result.unwrap_err().contains("Only in-app"));
351 }
352
353 #[test]
354 fn test_is_unread() {
355 let mut notification = Notification::new(
356 Uuid::new_v4(),
357 Uuid::new_v4(),
358 NotificationType::MeetingConvocation,
359 NotificationChannel::InApp,
360 NotificationPriority::High,
361 "Convocation AG".to_string(),
362 "Vous êtes convoqué à l'AG du 15/12.".to_string(),
363 )
364 .unwrap();
365
366 assert!(!notification.is_unread()); notification.mark_sent();
369 assert!(notification.is_unread()); notification.mark_read().unwrap();
372 assert!(!notification.is_unread()); }
374
375 #[test]
376 fn test_retry_failed_notification() {
377 let mut notification = Notification::new(
378 Uuid::new_v4(),
379 Uuid::new_v4(),
380 NotificationType::PaymentReminder,
381 NotificationChannel::Email,
382 NotificationPriority::High,
383 "Relance paiement".to_string(),
384 "Votre paiement est en retard.".to_string(),
385 )
386 .unwrap();
387
388 notification.mark_failed("Network error".to_string());
389
390 let result = notification.retry();
391 assert!(result.is_ok());
392 assert_eq!(notification.status, NotificationStatus::Pending);
393 assert!(notification.error_message.is_none());
394 }
395
396 #[test]
397 fn test_notification_preference() {
398 let pref = NotificationPreference::new(Uuid::new_v4(), NotificationType::ExpenseCreated);
399
400 assert!(pref.email_enabled);
401 assert!(pref.in_app_enabled);
402 assert!(pref.push_enabled);
403 }
404
405 #[test]
406 fn test_set_channel_enabled() {
407 let mut pref =
408 NotificationPreference::new(Uuid::new_v4(), NotificationType::MeetingConvocation);
409
410 pref.set_channel_enabled(NotificationChannel::Email, false);
411
412 assert!(!pref.email_enabled);
413 assert!(pref.in_app_enabled);
414 assert!(pref.push_enabled);
415 }
416
417 #[test]
418 fn test_is_channel_enabled() {
419 let pref = NotificationPreference::new(Uuid::new_v4(), NotificationType::TicketResolved);
420
421 assert!(pref.is_channel_enabled(&NotificationChannel::Email));
422 assert!(pref.is_channel_enabled(&NotificationChannel::InApp));
423 assert!(pref.is_channel_enabled(&NotificationChannel::Push));
424 }
425}