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// ==================== Notification Endpoints ====================
11
12#[post("/notifications")]
13pub async fn create_notification(
14    state: web::Data<AppState>,
15    user: AuthenticatedUser,
16    request: web::Json<CreateNotificationRequest>,
17) -> impl Responder {
18    let organization_id = match user.require_organization() {
19        Ok(org_id) => org_id,
20        Err(e) => {
21            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
22        }
23    };
24
25    match state
26        .notification_use_cases
27        .create_notification(organization_id, request.into_inner())
28        .await
29    {
30        Ok(notification) => {
31            AuditLogEntry::new(
32                AuditEventType::NotificationCreated,
33                Some(user.user_id),
34                Some(organization_id),
35            )
36            .with_resource("Notification", notification.id)
37            .log();
38
39            HttpResponse::Created().json(notification)
40        }
41        Err(err) => {
42            AuditLogEntry::new(
43                AuditEventType::NotificationCreated,
44                Some(user.user_id),
45                Some(organization_id),
46            )
47            .with_error(err.clone())
48            .log();
49
50            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
51        }
52    }
53}
54
55#[get("/notifications/{id}")]
56pub async fn get_notification(
57    state: web::Data<AppState>,
58    _user: AuthenticatedUser,
59    id: web::Path<Uuid>,
60) -> impl Responder {
61    match state.notification_use_cases.get_notification(*id).await {
62        Ok(Some(notification)) => HttpResponse::Ok().json(notification),
63        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
64            "error": "Notification not found"
65        })),
66        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
67    }
68}
69
70#[get("/notifications/my-notifications")]
71pub async fn list_my_notifications(
72    state: web::Data<AppState>,
73    user: AuthenticatedUser,
74) -> impl Responder {
75    match state
76        .notification_use_cases
77        .list_user_notifications(user.user_id)
78        .await
79    {
80        Ok(notifications) => HttpResponse::Ok().json(notifications),
81        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
82    }
83}
84
85#[get("/notifications/unread")]
86pub async fn list_unread_notifications(
87    state: web::Data<AppState>,
88    user: AuthenticatedUser,
89) -> impl Responder {
90    match state
91        .notification_use_cases
92        .list_unread_notifications(user.user_id)
93        .await
94    {
95        Ok(notifications) => HttpResponse::Ok().json(notifications),
96        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
97    }
98}
99
100#[put("/notifications/{id}/mark-read")]
101pub async fn mark_notification_read(
102    state: web::Data<AppState>,
103    user: AuthenticatedUser,
104    id: web::Path<Uuid>,
105    _request: web::Json<MarkReadRequest>,
106) -> impl Responder {
107    let organization_id = match user.require_organization() {
108        Ok(org_id) => org_id,
109        Err(e) => {
110            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
111        }
112    };
113
114    match state.notification_use_cases.mark_as_read(*id).await {
115        Ok(notification) => {
116            AuditLogEntry::new(
117                AuditEventType::NotificationRead,
118                Some(user.user_id),
119                Some(organization_id),
120            )
121            .with_resource("Notification", notification.id)
122            .log();
123
124            HttpResponse::Ok().json(notification)
125        }
126        Err(err) => {
127            AuditLogEntry::new(
128                AuditEventType::NotificationRead,
129                Some(user.user_id),
130                Some(organization_id),
131            )
132            .with_error(err.clone())
133            .log();
134
135            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
136        }
137    }
138}
139
140#[put("/notifications/mark-all-read")]
141pub async fn mark_all_notifications_read(
142    state: web::Data<AppState>,
143    user: AuthenticatedUser,
144) -> impl Responder {
145    let organization_id = match user.require_organization() {
146        Ok(org_id) => org_id,
147        Err(e) => {
148            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
149        }
150    };
151
152    match state
153        .notification_use_cases
154        .mark_all_read(user.user_id)
155        .await
156    {
157        Ok(count) => {
158            AuditLogEntry::new(
159                AuditEventType::NotificationRead,
160                Some(user.user_id),
161                Some(organization_id),
162            )
163            .with_details(format!("Marked {} notifications as read", count))
164            .log();
165
166            HttpResponse::Ok().json(serde_json::json!({"marked_read": count}))
167        }
168        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
169    }
170}
171
172#[delete("/notifications/{id}")]
173pub async fn delete_notification(
174    state: web::Data<AppState>,
175    user: AuthenticatedUser,
176    id: web::Path<Uuid>,
177) -> impl Responder {
178    let organization_id = match user.require_organization() {
179        Ok(org_id) => org_id,
180        Err(e) => {
181            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
182        }
183    };
184
185    match state.notification_use_cases.delete_notification(*id).await {
186        Ok(true) => {
187            AuditLogEntry::new(
188                AuditEventType::NotificationDeleted,
189                Some(user.user_id),
190                Some(organization_id),
191            )
192            .with_resource("Notification", *id)
193            .log();
194
195            HttpResponse::NoContent().finish()
196        }
197        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
198            "error": "Notification not found"
199        })),
200        Err(err) => {
201            AuditLogEntry::new(
202                AuditEventType::NotificationDeleted,
203                Some(user.user_id),
204                Some(organization_id),
205            )
206            .with_error(err.clone())
207            .log();
208
209            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
210        }
211    }
212}
213
214#[get("/notifications/stats")]
215pub async fn get_notification_stats(
216    state: web::Data<AppState>,
217    user: AuthenticatedUser,
218) -> impl Responder {
219    match state
220        .notification_use_cases
221        .get_user_stats(user.user_id)
222        .await
223    {
224        Ok(stats) => HttpResponse::Ok().json(stats),
225        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
226    }
227}
228
229// ==================== Notification Preference Endpoints ====================
230
231#[get("/notification-preferences")]
232pub async fn get_user_preferences(
233    state: web::Data<AppState>,
234    user: AuthenticatedUser,
235) -> impl Responder {
236    match state
237        .notification_use_cases
238        .get_user_preferences(user.user_id)
239        .await
240    {
241        Ok(preferences) => HttpResponse::Ok().json(preferences),
242        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
243    }
244}
245
246#[get("/notification-preferences/{notification_type}")]
247pub async fn get_preference(
248    state: web::Data<AppState>,
249    user: AuthenticatedUser,
250    notification_type: web::Path<String>,
251) -> impl Responder {
252    let notification_type = match notification_type.as_str() {
253        "expense_created" => NotificationType::ExpenseCreated,
254        "meeting_convocation" => NotificationType::MeetingConvocation,
255        "payment_received" => NotificationType::PaymentReceived,
256        "ticket_resolved" => NotificationType::TicketResolved,
257        "document_added" => NotificationType::DocumentAdded,
258        "board_message" => NotificationType::BoardMessage,
259        "payment_reminder" => NotificationType::PaymentReminder,
260        "budget_approved" => NotificationType::BudgetApproved,
261        "resolution_vote" => NotificationType::ResolutionVote,
262        "system" => NotificationType::System,
263        _ => {
264            return HttpResponse::BadRequest().json(serde_json::json!({
265                "error": format!("Invalid notification type: {}", notification_type)
266            }))
267        }
268    };
269
270    match state
271        .notification_use_cases
272        .get_preference(user.user_id, notification_type)
273        .await
274    {
275        Ok(Some(preference)) => HttpResponse::Ok().json(preference),
276        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
277            "error": "Preference not found"
278        })),
279        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
280    }
281}
282
283#[put("/notification-preferences/{notification_type}")]
284pub async fn update_preference(
285    state: web::Data<AppState>,
286    user: AuthenticatedUser,
287    notification_type: web::Path<String>,
288    request: web::Json<UpdatePreferenceRequest>,
289) -> impl Responder {
290    let organization_id = match user.require_organization() {
291        Ok(org_id) => org_id,
292        Err(e) => {
293            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
294        }
295    };
296
297    let notification_type = match notification_type.as_str() {
298        "expense_created" => NotificationType::ExpenseCreated,
299        "meeting_convocation" => NotificationType::MeetingConvocation,
300        "payment_received" => NotificationType::PaymentReceived,
301        "ticket_resolved" => NotificationType::TicketResolved,
302        "document_added" => NotificationType::DocumentAdded,
303        "board_message" => NotificationType::BoardMessage,
304        "payment_reminder" => NotificationType::PaymentReminder,
305        "budget_approved" => NotificationType::BudgetApproved,
306        "resolution_vote" => NotificationType::ResolutionVote,
307        "system" => NotificationType::System,
308        _ => {
309            return HttpResponse::BadRequest().json(serde_json::json!({
310                "error": format!("Invalid notification type: {}", notification_type)
311            }))
312        }
313    };
314
315    match state
316        .notification_use_cases
317        .update_preference(user.user_id, notification_type, request.into_inner())
318        .await
319    {
320        Ok(preference) => {
321            AuditLogEntry::new(
322                AuditEventType::NotificationPreferenceUpdated,
323                Some(user.user_id),
324                Some(organization_id),
325            )
326            .with_resource("NotificationPreference", preference.id)
327            .log();
328
329            HttpResponse::Ok().json(preference)
330        }
331        Err(err) => {
332            AuditLogEntry::new(
333                AuditEventType::NotificationPreferenceUpdated,
334                Some(user.user_id),
335                Some(organization_id),
336            )
337            .with_error(err.clone())
338            .log();
339
340            HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
341        }
342    }
343}