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
10fn 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#[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)) => {
108 if !user.is_superadmin() && notification.user_id != user.user_id {
114 return HttpResponse::Forbidden().json(serde_json::json!({
115 "error": "Notification does not belong to you"
116 }));
117 }
118 HttpResponse::Ok().json(notification)
119 }
120 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
121 "error": "Notification not found"
122 })),
123 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
124 }
125}
126
127#[utoipa::path(
128 get,
129 path = "/notifications/my",
130 tag = "Notifications",
131 summary = "List my notifications",
132 responses(
133 (status = 200, description = "Notifications retrieved"),
134 (status = 401, description = "Unauthorized"),
135 ),
136 security(("bearer_auth" = []))
137)]
138#[get("/notifications/my")]
139pub async fn list_my_notifications(
140 state: web::Data<AppState>,
141 user: AuthenticatedUser,
142) -> impl Responder {
143 match state
144 .notification_use_cases
145 .list_user_notifications(user.user_id)
146 .await
147 {
148 Ok(notifications) => HttpResponse::Ok().json(notifications),
149 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
150 }
151}
152
153#[utoipa::path(
154 get,
155 path = "/notifications/unread",
156 tag = "Notifications",
157 summary = "List unread notifications",
158 responses(
159 (status = 200, description = "Unread notifications retrieved"),
160 (status = 401, description = "Unauthorized"),
161 ),
162 security(("bearer_auth" = []))
163)]
164#[get("/notifications/unread")]
165pub async fn list_unread_notifications(
166 state: web::Data<AppState>,
167 user: AuthenticatedUser,
168) -> impl Responder {
169 match state
170 .notification_use_cases
171 .list_unread_notifications(user.user_id)
172 .await
173 {
174 Ok(notifications) => HttpResponse::Ok().json(notifications),
175 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
176 }
177}
178
179#[utoipa::path(
180 put,
181 path = "/notifications/{id}/read",
182 tag = "Notifications",
183 summary = "Mark a notification as read",
184 params(
185 ("id" = Uuid, Path, description = "Notification ID")
186 ),
187 request_body = MarkReadRequest,
188 responses(
189 (status = 200, description = "Notification marked as read"),
190 (status = 400, description = "Invalid request"),
191 (status = 401, description = "Unauthorized"),
192 ),
193 security(("bearer_auth" = []))
194)]
195#[put("/notifications/{id}/read")]
196pub async fn mark_notification_read(
197 state: web::Data<AppState>,
198 user: AuthenticatedUser,
199 id: web::Path<Uuid>,
200 _request: web::Json<MarkReadRequest>,
201) -> impl Responder {
202 let organization_id = match user.require_organization() {
203 Ok(org_id) => org_id,
204 Err(e) => {
205 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
206 }
207 };
208
209 match state.notification_use_cases.mark_as_read(*id).await {
210 Ok(notification) => {
211 AuditLogEntry::new(
212 AuditEventType::NotificationRead,
213 Some(user.user_id),
214 Some(organization_id),
215 )
216 .with_resource("Notification", notification.id)
217 .log();
218
219 HttpResponse::Ok().json(notification)
220 }
221 Err(err) => {
222 AuditLogEntry::new(
223 AuditEventType::NotificationRead,
224 Some(user.user_id),
225 Some(organization_id),
226 )
227 .with_error(err.clone())
228 .log();
229
230 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
231 }
232 }
233}
234
235#[utoipa::path(
236 put,
237 path = "/notifications/read-all",
238 tag = "Notifications",
239 summary = "Mark all notifications as read",
240 responses(
241 (status = 200, description = "All notifications marked as read"),
242 (status = 401, description = "Unauthorized"),
243 ),
244 security(("bearer_auth" = []))
245)]
246#[put("/notifications/read-all")]
247pub async fn mark_all_notifications_read(
248 state: web::Data<AppState>,
249 user: AuthenticatedUser,
250) -> impl Responder {
251 let organization_id = match user.require_organization() {
252 Ok(org_id) => org_id,
253 Err(e) => {
254 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
255 }
256 };
257
258 match state
259 .notification_use_cases
260 .mark_all_read(user.user_id)
261 .await
262 {
263 Ok(count) => {
264 AuditLogEntry::new(
265 AuditEventType::NotificationRead,
266 Some(user.user_id),
267 Some(organization_id),
268 )
269 .with_details(format!("Marked {} notifications as read", count))
270 .log();
271
272 HttpResponse::Ok().json(serde_json::json!({"marked_read": count}))
273 }
274 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
275 }
276}
277
278#[utoipa::path(
279 delete,
280 path = "/notifications/{id}",
281 tag = "Notifications",
282 summary = "Delete a notification",
283 params(
284 ("id" = Uuid, Path, description = "Notification ID")
285 ),
286 responses(
287 (status = 204, description = "Notification deleted"),
288 (status = 400, description = "Invalid request"),
289 (status = 401, description = "Unauthorized"),
290 (status = 404, description = "Notification not found"),
291 ),
292 security(("bearer_auth" = []))
293)]
294#[delete("/notifications/{id}")]
295pub async fn delete_notification(
296 state: web::Data<AppState>,
297 user: AuthenticatedUser,
298 id: web::Path<Uuid>,
299) -> impl Responder {
300 let organization_id = match user.require_organization() {
301 Ok(org_id) => org_id,
302 Err(e) => {
303 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
304 }
305 };
306
307 match state.notification_use_cases.delete_notification(*id).await {
308 Ok(true) => {
309 AuditLogEntry::new(
310 AuditEventType::NotificationDeleted,
311 Some(user.user_id),
312 Some(organization_id),
313 )
314 .with_resource("Notification", *id)
315 .log();
316
317 HttpResponse::NoContent().finish()
318 }
319 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
320 "error": "Notification not found"
321 })),
322 Err(err) => {
323 AuditLogEntry::new(
324 AuditEventType::NotificationDeleted,
325 Some(user.user_id),
326 Some(organization_id),
327 )
328 .with_error(err.clone())
329 .log();
330
331 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
332 }
333 }
334}
335
336#[utoipa::path(
337 get,
338 path = "/notifications/stats",
339 tag = "Notifications",
340 summary = "Get notification statistics for current user",
341 responses(
342 (status = 200, description = "Statistics retrieved"),
343 (status = 401, description = "Unauthorized"),
344 ),
345 security(("bearer_auth" = []))
346)]
347#[get("/notifications/stats")]
348pub async fn get_notification_stats(
349 state: web::Data<AppState>,
350 user: AuthenticatedUser,
351) -> impl Responder {
352 match state
353 .notification_use_cases
354 .get_user_stats(user.user_id)
355 .await
356 {
357 Ok(stats) => HttpResponse::Ok().json(stats),
358 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
359 }
360}
361
362#[utoipa::path(
365 get,
366 path = "/notification-preferences",
367 tag = "Notifications",
368 summary = "Get all notification preferences for current user",
369 responses(
370 (status = 200, description = "Preferences retrieved"),
371 (status = 401, description = "Unauthorized"),
372 ),
373 security(("bearer_auth" = []))
374)]
375#[get("/notification-preferences")]
376pub async fn get_user_preferences(
377 state: web::Data<AppState>,
378 user: AuthenticatedUser,
379) -> impl Responder {
380 match state
381 .notification_use_cases
382 .get_user_preferences(user.user_id)
383 .await
384 {
385 Ok(preferences) => HttpResponse::Ok().json(preferences),
386 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
387 }
388}
389
390#[utoipa::path(
391 get,
392 path = "/notification-preferences/{notification_type}",
393 tag = "Notifications",
394 summary = "Get a specific notification preference by type",
395 params(
396 ("notification_type" = String, Path, description = "Notification type (e.g. expense_created, meeting_convocation)")
397 ),
398 responses(
399 (status = 200, description = "Preference retrieved"),
400 (status = 400, description = "Invalid notification type"),
401 (status = 401, description = "Unauthorized"),
402 (status = 404, description = "Preference not found"),
403 ),
404 security(("bearer_auth" = []))
405)]
406#[get("/notification-preferences/{notification_type}")]
407pub async fn get_preference(
408 state: web::Data<AppState>,
409 user: AuthenticatedUser,
410 notification_type: web::Path<String>,
411) -> impl Responder {
412 let notification_type = match parse_notification_type(notification_type.as_str()) {
413 Some(nt) => nt,
414 None => {
415 return HttpResponse::BadRequest().json(serde_json::json!({
416 "error": format!("Invalid notification type: {}", notification_type)
417 }))
418 }
419 };
420
421 match state
422 .notification_use_cases
423 .get_preference(user.user_id, notification_type)
424 .await
425 {
426 Ok(Some(preference)) => HttpResponse::Ok().json(preference),
427 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
428 "error": "Preference not found"
429 })),
430 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
431 }
432}
433
434#[utoipa::path(
435 put,
436 path = "/notification-preferences/{notification_type}",
437 tag = "Notifications",
438 summary = "Update a notification preference by type",
439 params(
440 ("notification_type" = String, Path, description = "Notification type (e.g. expense_created, meeting_convocation)")
441 ),
442 request_body = UpdatePreferenceRequest,
443 responses(
444 (status = 200, description = "Preference updated"),
445 (status = 400, description = "Invalid notification type or request"),
446 (status = 401, description = "Unauthorized"),
447 ),
448 security(("bearer_auth" = []))
449)]
450#[put("/notification-preferences/{notification_type}")]
451pub async fn update_preference(
452 state: web::Data<AppState>,
453 user: AuthenticatedUser,
454 notification_type: web::Path<String>,
455 request: web::Json<UpdatePreferenceRequest>,
456) -> impl Responder {
457 let organization_id = match user.require_organization() {
458 Ok(org_id) => org_id,
459 Err(e) => {
460 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
461 }
462 };
463
464 let notification_type = match parse_notification_type(notification_type.as_str()) {
465 Some(nt) => nt,
466 None => {
467 return HttpResponse::BadRequest().json(serde_json::json!({
468 "error": format!("Invalid notification type: {}", notification_type)
469 }))
470 }
471 };
472
473 match state
474 .notification_use_cases
475 .update_preference(user.user_id, notification_type, request.into_inner())
476 .await
477 {
478 Ok(preference) => {
479 AuditLogEntry::new(
480 AuditEventType::NotificationPreferenceUpdated,
481 Some(user.user_id),
482 Some(organization_id),
483 )
484 .with_resource("NotificationPreference", preference.id)
485 .log();
486
487 HttpResponse::Ok().json(preference)
488 }
489 Err(err) => {
490 AuditLogEntry::new(
491 AuditEventType::NotificationPreferenceUpdated,
492 Some(user.user_id),
493 Some(organization_id),
494 )
495 .with_error(err.clone())
496 .log();
497
498 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
499 }
500 }
501}