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)) => 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#[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}