koprogo_api/infrastructure/database/repositories/
notification_preference_repository_impl.rs

1use crate::application::ports::NotificationPreferenceRepository;
2use crate::domain::entities::{NotificationChannel, NotificationPreference, NotificationType};
3use async_trait::async_trait;
4use sqlx::PgPool;
5use uuid::Uuid;
6
7/// PostgreSQL implementation of NotificationPreferenceRepository
8pub struct PostgresNotificationPreferenceRepository {
9    pool: PgPool,
10}
11
12impl PostgresNotificationPreferenceRepository {
13    pub fn new(pool: PgPool) -> Self {
14        Self { pool }
15    }
16
17    /// Convert NotificationType enum to database string
18    fn type_to_db(notification_type: &NotificationType) -> &'static str {
19        match notification_type {
20            NotificationType::ExpenseCreated => "ExpenseCreated",
21            NotificationType::MeetingConvocation => "MeetingConvocation",
22            NotificationType::PaymentReceived => "PaymentReceived",
23            NotificationType::TicketResolved => "TicketResolved",
24            NotificationType::DocumentAdded => "DocumentAdded",
25            NotificationType::BoardMessage => "BoardMessage",
26            NotificationType::PaymentReminder => "PaymentReminder",
27            NotificationType::BudgetApproved => "BudgetApproved",
28            NotificationType::ResolutionVote => "ResolutionVote",
29            NotificationType::System => "System",
30        }
31    }
32
33    /// Convert database string to NotificationType enum
34    fn type_from_db(s: &str) -> Result<NotificationType, String> {
35        match s {
36            "ExpenseCreated" => Ok(NotificationType::ExpenseCreated),
37            "MeetingConvocation" => Ok(NotificationType::MeetingConvocation),
38            "PaymentReceived" => Ok(NotificationType::PaymentReceived),
39            "TicketResolved" => Ok(NotificationType::TicketResolved),
40            "DocumentAdded" => Ok(NotificationType::DocumentAdded),
41            "BoardMessage" => Ok(NotificationType::BoardMessage),
42            "PaymentReminder" => Ok(NotificationType::PaymentReminder),
43            "BudgetApproved" => Ok(NotificationType::BudgetApproved),
44            "ResolutionVote" => Ok(NotificationType::ResolutionVote),
45            "System" => Ok(NotificationType::System),
46            _ => Err(format!("Invalid notification type: {}", s)),
47        }
48    }
49}
50
51#[async_trait]
52impl NotificationPreferenceRepository for PostgresNotificationPreferenceRepository {
53    async fn create(
54        &self,
55        preference: &NotificationPreference,
56    ) -> Result<NotificationPreference, String> {
57        let type_str = Self::type_to_db(&preference.notification_type);
58
59        let row = sqlx::query!(
60            r#"
61            INSERT INTO notification_preferences (
62                id, user_id, notification_type, email_enabled, in_app_enabled, push_enabled,
63                created_at, updated_at
64            )
65            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
66            RETURNING id, user_id, notification_type, email_enabled, in_app_enabled, push_enabled,
67                      created_at, updated_at
68            "#,
69            preference.id,
70            preference.user_id,
71            type_str,
72            preference.email_enabled,
73            preference.in_app_enabled,
74            preference.push_enabled,
75            preference.created_at,
76            preference.updated_at
77        )
78        .fetch_one(&self.pool)
79        .await
80        .map_err(|e| format!("Database error creating notification preference: {}", e))?;
81
82        Ok(NotificationPreference {
83            id: row.id,
84            user_id: row.user_id,
85            notification_type: Self::type_from_db(&row.notification_type)?,
86            email_enabled: row.email_enabled,
87            in_app_enabled: row.in_app_enabled,
88            push_enabled: row.push_enabled,
89            created_at: row.created_at,
90            updated_at: row.updated_at,
91        })
92    }
93
94    async fn find_by_id(&self, id: Uuid) -> Result<Option<NotificationPreference>, String> {
95        let row = sqlx::query!(
96            r#"
97            SELECT id, user_id, notification_type, email_enabled, in_app_enabled, push_enabled,
98                   created_at, updated_at
99            FROM notification_preferences
100            WHERE id = $1
101            "#,
102            id
103        )
104        .fetch_optional(&self.pool)
105        .await
106        .map_err(|e| format!("Database error finding notification preference: {}", e))?;
107
108        match row {
109            Some(r) => Ok(Some(NotificationPreference {
110                id: r.id,
111                user_id: r.user_id,
112                notification_type: Self::type_from_db(&r.notification_type)?,
113                email_enabled: r.email_enabled,
114                in_app_enabled: r.in_app_enabled,
115                push_enabled: r.push_enabled,
116                created_at: r.created_at,
117                updated_at: r.updated_at,
118            })),
119            None => Ok(None),
120        }
121    }
122
123    async fn find_by_user_and_type(
124        &self,
125        user_id: Uuid,
126        notification_type: NotificationType,
127    ) -> Result<Option<NotificationPreference>, String> {
128        let type_str = Self::type_to_db(&notification_type);
129
130        let row = sqlx::query!(
131            r#"
132            SELECT id, user_id, notification_type, email_enabled, in_app_enabled, push_enabled,
133                   created_at, updated_at
134            FROM notification_preferences
135            WHERE user_id = $1 AND notification_type = $2
136            "#,
137            user_id,
138            type_str
139        )
140        .fetch_optional(&self.pool)
141        .await
142        .map_err(|e| {
143            format!(
144                "Database error finding notification preference by user and type: {}",
145                e
146            )
147        })?;
148
149        match row {
150            Some(r) => Ok(Some(NotificationPreference {
151                id: r.id,
152                user_id: r.user_id,
153                notification_type: Self::type_from_db(&r.notification_type)?,
154                email_enabled: r.email_enabled,
155                in_app_enabled: r.in_app_enabled,
156                push_enabled: r.push_enabled,
157                created_at: r.created_at,
158                updated_at: r.updated_at,
159            })),
160            None => Ok(None),
161        }
162    }
163
164    async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<NotificationPreference>, String> {
165        let rows = sqlx::query!(
166            r#"
167            SELECT id, user_id, notification_type, email_enabled, in_app_enabled, push_enabled,
168                   created_at, updated_at
169            FROM notification_preferences
170            WHERE user_id = $1
171            ORDER BY notification_type
172            "#,
173            user_id
174        )
175        .fetch_all(&self.pool)
176        .await
177        .map_err(|e| {
178            format!(
179                "Database error finding notification preferences by user: {}",
180                e
181            )
182        })?;
183
184        rows.into_iter()
185            .map(|r| {
186                Ok(NotificationPreference {
187                    id: r.id,
188                    user_id: r.user_id,
189                    notification_type: Self::type_from_db(&r.notification_type)?,
190                    email_enabled: r.email_enabled,
191                    in_app_enabled: r.in_app_enabled,
192                    push_enabled: r.push_enabled,
193                    created_at: r.created_at,
194                    updated_at: r.updated_at,
195                })
196            })
197            .collect()
198    }
199
200    async fn update(
201        &self,
202        preference: &NotificationPreference,
203    ) -> Result<NotificationPreference, String> {
204        let type_str = Self::type_to_db(&preference.notification_type);
205
206        let row = sqlx::query!(
207            r#"
208            UPDATE notification_preferences
209            SET user_id = $2,
210                notification_type = $3,
211                email_enabled = $4,
212                in_app_enabled = $5,
213                push_enabled = $6,
214                updated_at = $7
215            WHERE id = $1
216            RETURNING id, user_id, notification_type, email_enabled, in_app_enabled, push_enabled,
217                      created_at, updated_at
218            "#,
219            preference.id,
220            preference.user_id,
221            type_str,
222            preference.email_enabled,
223            preference.in_app_enabled,
224            preference.push_enabled,
225            preference.updated_at
226        )
227        .fetch_one(&self.pool)
228        .await
229        .map_err(|e| format!("Database error updating notification preference: {}", e))?;
230
231        Ok(NotificationPreference {
232            id: row.id,
233            user_id: row.user_id,
234            notification_type: Self::type_from_db(&row.notification_type)?,
235            email_enabled: row.email_enabled,
236            in_app_enabled: row.in_app_enabled,
237            push_enabled: row.push_enabled,
238            created_at: row.created_at,
239            updated_at: row.updated_at,
240        })
241    }
242
243    async fn delete(&self, id: Uuid) -> Result<bool, String> {
244        let result = sqlx::query!(
245            r#"
246            DELETE FROM notification_preferences
247            WHERE id = $1
248            "#,
249            id
250        )
251        .execute(&self.pool)
252        .await
253        .map_err(|e| format!("Database error deleting notification preference: {}", e))?;
254
255        Ok(result.rows_affected() > 0)
256    }
257
258    async fn is_channel_enabled(
259        &self,
260        user_id: Uuid,
261        notification_type: NotificationType,
262        channel: NotificationChannel,
263    ) -> Result<bool, String> {
264        let type_str = Self::type_to_db(&notification_type);
265
266        let column = match channel {
267            NotificationChannel::Email => "email_enabled",
268            NotificationChannel::InApp => "in_app_enabled",
269            NotificationChannel::Push => "push_enabled",
270        };
271
272        let query = format!(
273            r#"
274            SELECT {}
275            FROM notification_preferences
276            WHERE user_id = $1 AND notification_type = $2
277            "#,
278            column
279        );
280
281        let row = sqlx::query_scalar::<_, bool>(&query)
282            .bind(user_id)
283            .bind(type_str)
284            .fetch_optional(&self.pool)
285            .await
286            .map_err(|e| format!("Database error checking channel enabled: {}", e))?;
287
288        // If no preference exists, default to true (all channels enabled)
289        Ok(row.unwrap_or(true))
290    }
291
292    async fn create_defaults_for_user(
293        &self,
294        user_id: Uuid,
295    ) -> Result<Vec<NotificationPreference>, String> {
296        let notification_types = vec![
297            NotificationType::ExpenseCreated,
298            NotificationType::MeetingConvocation,
299            NotificationType::PaymentReceived,
300            NotificationType::TicketResolved,
301            NotificationType::DocumentAdded,
302            NotificationType::BoardMessage,
303            NotificationType::PaymentReminder,
304            NotificationType::BudgetApproved,
305            NotificationType::ResolutionVote,
306            NotificationType::System,
307        ];
308
309        let mut created_prefs = Vec::new();
310
311        for notification_type in notification_types {
312            let pref = NotificationPreference::new(user_id, notification_type);
313            let created = self.create(&pref).await?;
314            created_prefs.push(created);
315        }
316
317        Ok(created_prefs)
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_type_conversion() {
327        assert_eq!(
328            PostgresNotificationPreferenceRepository::type_to_db(&NotificationType::ExpenseCreated),
329            "ExpenseCreated"
330        );
331        assert_eq!(
332            PostgresNotificationPreferenceRepository::type_from_db("MeetingConvocation").unwrap(),
333            NotificationType::MeetingConvocation
334        );
335    }
336
337    #[test]
338    fn test_invalid_type() {
339        assert!(PostgresNotificationPreferenceRepository::type_from_db("Invalid").is_err());
340    }
341}