koprogo_api/infrastructure/web/handlers/
notification_handlers.rs

1use crate::application::dto::{
2    CreateNotificationRequest, MarkReadRequest, UpdatePreferenceRequest,
3};
4use crate::domain::entities::NotificationType;
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use uuid::Uuid;
9
10/// Parse notification type from both snake_case and PascalCase
11fn parse_notification_type(s: &str) -> Option<NotificationType> {
12    match s {
13        "expense_created" | "ExpenseCreated" => Some(NotificationType::ExpenseCreated),
14        "meeting_convocation" | "MeetingConvocation" => Some(NotificationType::MeetingConvocation),
15        "payment_received" | "PaymentReceived" => Some(NotificationType::PaymentReceived),
16        "ticket_resolved" | "TicketResolved" => Some(NotificationType::TicketResolved),
17        "document_added" | "DocumentAdded" => Some(NotificationType::DocumentAdded),
18        "board_message" | "BoardMessage" => Some(NotificationType::BoardMessage),
19        "payment_reminder" | "PaymentReminder" => Some(NotificationType::PaymentReminder),
20        "budget_approved" | "BudgetApproved" => Some(NotificationType::BudgetApproved),
21        "resolution_vote" | "ResolutionVote" => Some(NotificationType::ResolutionVote),
22        "system" | "System" => Some(NotificationType::System),
23        _ => None,
24    }
25}
26
27// ==================== Notification Endpoints ====================
28
29#[utoipa::path(
30    post,
31    path = "/notifications",
32    tag = "Notifications",
33    summary = "Create a notification",
34    request_body = CreateNotificationRequest,
35    responses(
36        (status = 201, description = "Notification created"),
37        (status = 400, description = "Invalid request"),
38        (status = 401, description = "Unauthorized"),
39    ),
40    security(("bearer_auth" = []))
41)]
42#[post("/notifications")]
43pub async fn create_notification(
44    state: web::Data<AppState>,
45    user: AuthenticatedUser,
46    request: web::Json<CreateNotificationRequest>,
47) -> impl Responder {
48    let organization_id = match user.require_organization() {
49        Ok(org_id) => org_id,
50        Err(e) => {
51            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
52        }
53    };
54
55    match state
56        .notification_use_cases
57        .create_notification(organization_id, request.into_inner())
58        .await
59    {
60        Ok(notification) => {
61            AuditLogEntry::new(
62                AuditEventType::NotificationCreated,
63                Some(user.user_id),
64                Some(organization_id),
65            )
66            .with_resource("Notification", notification.id)
67            .log();
68
69            HttpResponse::Created().json(notification)
70        }
71        Err(err) => {
72            AuditLogEntry::new(
73                AuditEventType::NotificationCreated,
74                Some(user.user_id),
75                Some(organization_id),
76            )
77            .with_error(err.clone())
78            .log();
79
80            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
81        }
82    }
83}
84
85#[utoipa::path(
86    get,
87    path = "/notifications/{id}",
88    tag = "Notifications",
89    summary = "Get a notification by ID",
90    params(
91        ("id" = Uuid, Path, description = "Notification ID")
92    ),
93    responses(
94        (status = 200, description = "Notification retrieved"),
95        (status = 401, description = "Unauthorized"),
96        (status = 404, description = "Notification not found"),
97    ),
98    security(("bearer_auth" = []))
99)]
100#[get("/notifications/{id}")]
101pub async fn get_notification(
102    state: web::Data<AppState>,
103    _user: AuthenticatedUser,
104    id: web::Path<Uuid>,
105) -> impl Responder {
106    match state.notification_use_cases.get_notification(*id).await {
107        Ok(Some(notification)) => HttpResponse::Ok().json(notification),
108        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
109            "error": "Notification not found"
110        })),
111        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
112    }
113}
114
115#[utoipa::path(
116    get,
117    path = "/notifications/my",
118    tag = "Notifications",
119    summary = "List my notifications",
120    responses(
121        (status = 200, description = "Notifications retrieved"),
122        (status = 401, description = "Unauthorized"),
123    ),
124    security(("bearer_auth" = []))
125)]
126#[get("/notifications/my")]
127pub async fn list_my_notifications(
128    state: web::Data<AppState>,
129    user: AuthenticatedUser,
130) -> impl Responder {
131    match state
132        .notification_use_cases
133        .list_user_notifications(user.user_id)
134        .await
135    {
136        Ok(notifications) => HttpResponse::Ok().json(notifications),
137        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
138    }
139}
140
141#[utoipa::path(
142    get,
143    path = "/notifications/unread",
144    tag = "Notifications",
145    summary = "List unread notifications",
146    responses(
147        (status = 200, description = "Unread notifications retrieved"),
148        (status = 401, description = "Unauthorized"),
149    ),
150    security(("bearer_auth" = []))
151)]
152#[get("/notifications/unread")]
153pub async fn list_unread_notifications(
154    state: web::Data<AppState>,
155    user: AuthenticatedUser,
156) -> impl Responder {
157    match state
158        .notification_use_cases
159        .list_unread_notifications(user.user_id)
160        .await
161    {
162        Ok(notifications) => HttpResponse::Ok().json(notifications),
163        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
164    }
165}
166
167#[utoipa::path(
168    put,
169    path = "/notifications/{id}/read",
170    tag = "Notifications",
171    summary = "Mark a notification as read",
172    params(
173        ("id" = Uuid, Path, description = "Notification ID")
174    ),
175    request_body = MarkReadRequest,
176    responses(
177        (status = 200, description = "Notification marked as read"),
178        (status = 400, description = "Invalid request"),
179        (status = 401, description = "Unauthorized"),
180    ),
181    security(("bearer_auth" = []))
182)]
183#[put("/notifications/{id}/read")]
184pub async fn mark_notification_read(
185    state: web::Data<AppState>,
186    user: AuthenticatedUser,
187    id: web::Path<Uuid>,
188    _request: web::Json<MarkReadRequest>,
189) -> impl Responder {
190    let organization_id = match user.require_organization() {
191        Ok(org_id) => org_id,
192        Err(e) => {
193            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
194        }
195    };
196
197    match state.notification_use_cases.mark_as_read(*id).await {
198        Ok(notification) => {
199            AuditLogEntry::new(
200                AuditEventType::NotificationRead,
201                Some(user.user_id),
202                Some(organization_id),
203            )
204            .with_resource("Notification", notification.id)
205            .log();
206
207            HttpResponse::Ok().json(notification)
208        }
209        Err(err) => {
210            AuditLogEntry::new(
211                AuditEventType::NotificationRead,
212                Some(user.user_id),
213                Some(organization_id),
214            )
215            .with_error(err.clone())
216            .log();
217
218            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
219        }
220    }
221}
222
223#[utoipa::path(
224    put,
225    path = "/notifications/read-all",
226    tag = "Notifications",
227    summary = "Mark all notifications as read",
228    responses(
229        (status = 200, description = "All notifications marked as read"),
230        (status = 401, description = "Unauthorized"),
231    ),
232    security(("bearer_auth" = []))
233)]
234#[put("/notifications/read-all")]
235pub async fn mark_all_notifications_read(
236    state: web::Data<AppState>,
237    user: AuthenticatedUser,
238) -> impl Responder {
239    let organization_id = match user.require_organization() {
240        Ok(org_id) => org_id,
241        Err(e) => {
242            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
243        }
244    };
245
246    match state
247        .notification_use_cases
248        .mark_all_read(user.user_id)
249        .await
250    {
251        Ok(count) => {
252            AuditLogEntry::new(
253                AuditEventType::NotificationRead,
254                Some(user.user_id),
255                Some(organization_id),
256            )
257            .with_details(format!("Marked {} notifications as read", count))
258            .log();
259
260            HttpResponse::Ok().json(serde_json::json!({"marked_read": count}))
261        }
262        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
263    }
264}
265
266#[utoipa::path(
267    delete,
268    path = "/notifications/{id}",
269    tag = "Notifications",
270    summary = "Delete a notification",
271    params(
272        ("id" = Uuid, Path, description = "Notification ID")
273    ),
274    responses(
275        (status = 204, description = "Notification deleted"),
276        (status = 400, description = "Invalid request"),
277        (status = 401, description = "Unauthorized"),
278        (status = 404, description = "Notification not found"),
279    ),
280    security(("bearer_auth" = []))
281)]
282#[delete("/notifications/{id}")]
283pub async fn delete_notification(
284    state: web::Data<AppState>,
285    user: AuthenticatedUser,
286    id: web::Path<Uuid>,
287) -> impl Responder {
288    let organization_id = match user.require_organization() {
289        Ok(org_id) => org_id,
290        Err(e) => {
291            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
292        }
293    };
294
295    match state.notification_use_cases.delete_notification(*id).await {
296        Ok(true) => {
297            AuditLogEntry::new(
298                AuditEventType::NotificationDeleted,
299                Some(user.user_id),
300                Some(organization_id),
301            )
302            .with_resource("Notification", *id)
303            .log();
304
305            HttpResponse::NoContent().finish()
306        }
307        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
308            "error": "Notification not found"
309        })),
310        Err(err) => {
311            AuditLogEntry::new(
312                AuditEventType::NotificationDeleted,
313                Some(user.user_id),
314                Some(organization_id),
315            )
316            .with_error(err.clone())
317            .log();
318
319            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
320        }
321    }
322}
323
324#[utoipa::path(
325    get,
326    path = "/notifications/stats",
327    tag = "Notifications",
328    summary = "Get notification statistics for current user",
329    responses(
330        (status = 200, description = "Statistics retrieved"),
331        (status = 401, description = "Unauthorized"),
332    ),
333    security(("bearer_auth" = []))
334)]
335#[get("/notifications/stats")]
336pub async fn get_notification_stats(
337    state: web::Data<AppState>,
338    user: AuthenticatedUser,
339) -> impl Responder {
340    match state
341        .notification_use_cases
342        .get_user_stats(user.user_id)
343        .await
344    {
345        Ok(stats) => HttpResponse::Ok().json(stats),
346        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
347    }
348}
349
350// ==================== Notification Preference Endpoints ====================
351
352#[utoipa::path(
353    get,
354    path = "/notification-preferences",
355    tag = "Notifications",
356    summary = "Get all notification preferences for current user",
357    responses(
358        (status = 200, description = "Preferences retrieved"),
359        (status = 401, description = "Unauthorized"),
360    ),
361    security(("bearer_auth" = []))
362)]
363#[get("/notification-preferences")]
364pub async fn get_user_preferences(
365    state: web::Data<AppState>,
366    user: AuthenticatedUser,
367) -> impl Responder {
368    match state
369        .notification_use_cases
370        .get_user_preferences(user.user_id)
371        .await
372    {
373        Ok(preferences) => HttpResponse::Ok().json(preferences),
374        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
375    }
376}
377
378#[utoipa::path(
379    get,
380    path = "/notification-preferences/{notification_type}",
381    tag = "Notifications",
382    summary = "Get a specific notification preference by type",
383    params(
384        ("notification_type" = String, Path, description = "Notification type (e.g. expense_created, meeting_convocation)")
385    ),
386    responses(
387        (status = 200, description = "Preference retrieved"),
388        (status = 400, description = "Invalid notification type"),
389        (status = 401, description = "Unauthorized"),
390        (status = 404, description = "Preference not found"),
391    ),
392    security(("bearer_auth" = []))
393)]
394#[get("/notification-preferences/{notification_type}")]
395pub async fn get_preference(
396    state: web::Data<AppState>,
397    user: AuthenticatedUser,
398    notification_type: web::Path<String>,
399) -> impl Responder {
400    let notification_type = match parse_notification_type(notification_type.as_str()) {
401        Some(nt) => nt,
402        None => {
403            return HttpResponse::BadRequest().json(serde_json::json!({
404                "error": format!("Invalid notification type: {}", notification_type)
405            }))
406        }
407    };
408
409    match state
410        .notification_use_cases
411        .get_preference(user.user_id, notification_type)
412        .await
413    {
414        Ok(Some(preference)) => HttpResponse::Ok().json(preference),
415        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
416            "error": "Preference not found"
417        })),
418        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
419    }
420}
421
422#[utoipa::path(
423    put,
424    path = "/notification-preferences/{notification_type}",
425    tag = "Notifications",
426    summary = "Update a notification preference by type",
427    params(
428        ("notification_type" = String, Path, description = "Notification type (e.g. expense_created, meeting_convocation)")
429    ),
430    request_body = UpdatePreferenceRequest,
431    responses(
432        (status = 200, description = "Preference updated"),
433        (status = 400, description = "Invalid notification type or request"),
434        (status = 401, description = "Unauthorized"),
435    ),
436    security(("bearer_auth" = []))
437)]
438#[put("/notification-preferences/{notification_type}")]
439pub async fn update_preference(
440    state: web::Data<AppState>,
441    user: AuthenticatedUser,
442    notification_type: web::Path<String>,
443    request: web::Json<UpdatePreferenceRequest>,
444) -> impl Responder {
445    let organization_id = match user.require_organization() {
446        Ok(org_id) => org_id,
447        Err(e) => {
448            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
449        }
450    };
451
452    let notification_type = match parse_notification_type(notification_type.as_str()) {
453        Some(nt) => nt,
454        None => {
455            return HttpResponse::BadRequest().json(serde_json::json!({
456                "error": format!("Invalid notification type: {}", notification_type)
457            }))
458        }
459    };
460
461    match state
462        .notification_use_cases
463        .update_preference(user.user_id, notification_type, request.into_inner())
464        .await
465    {
466        Ok(preference) => {
467            AuditLogEntry::new(
468                AuditEventType::NotificationPreferenceUpdated,
469                Some(user.user_id),
470                Some(organization_id),
471            )
472            .with_resource("NotificationPreference", preference.id)
473            .log();
474
475            HttpResponse::Ok().json(preference)
476        }
477        Err(err) => {
478            AuditLogEntry::new(
479                AuditEventType::NotificationPreferenceUpdated,
480                Some(user.user_id),
481                Some(organization_id),
482            )
483            .with_error(err.clone())
484            .log();
485
486            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
487        }
488    }
489}