1use crate::application::ports::NotificationPreferenceRepository;
2use crate::domain::entities::{NotificationChannel, NotificationPreference, NotificationType};
3use async_trait::async_trait;
4use sqlx::PgPool;
5use uuid::Uuid;
6
7pub struct PostgresNotificationPreferenceRepository {
9 pool: PgPool,
10}
11
12impl PostgresNotificationPreferenceRepository {
13 pub fn new(pool: PgPool) -> Self {
14 Self { pool }
15 }
16
17 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 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(¬ification_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(¬ification_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 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}