koprogo_api/infrastructure/web/handlers/
notification_handlers.rs1use 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#[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#[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}