koprogo_api/infrastructure/web/handlers/
ticket_handlers.rs1use crate::application::dto::{
2 AssignTicketRequest, CancelTicketRequest, CreateTicketRequest, ReopenTicketRequest,
3 ResolveTicketRequest,
4};
5use crate::domain::entities::TicketStatus;
6use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
7use crate::infrastructure::web::{AppState, AuthenticatedUser};
8use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
9use uuid::Uuid;
10
11#[post("/tickets")]
14pub async fn create_ticket(
15 state: web::Data<AppState>,
16 user: AuthenticatedUser,
17 request: web::Json<CreateTicketRequest>,
18) -> impl Responder {
19 let organization_id = match user.require_organization() {
20 Ok(org_id) => org_id,
21 Err(e) => {
22 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
23 }
24 };
25
26 let created_by = user.user_id;
28
29 match state
30 .ticket_use_cases
31 .create_ticket(organization_id, created_by, request.into_inner())
32 .await
33 {
34 Ok(ticket) => {
35 AuditLogEntry::new(
36 AuditEventType::TicketCreated,
37 Some(user.user_id),
38 Some(organization_id),
39 )
40 .with_resource("Ticket", ticket.id)
41 .log();
42
43 HttpResponse::Created().json(ticket)
44 }
45 Err(err) => {
46 AuditLogEntry::new(
47 AuditEventType::TicketCreated,
48 Some(user.user_id),
49 Some(organization_id),
50 )
51 .with_error(err.clone())
52 .log();
53
54 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
55 }
56 }
57}
58
59#[get("/tickets/{id}")]
60pub async fn get_ticket(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
61 match state.ticket_use_cases.get_ticket(*id).await {
62 Ok(Some(ticket)) => HttpResponse::Ok().json(ticket),
63 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
64 "error": "Ticket not found"
65 })),
66 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
67 }
68}
69
70#[get("/buildings/{building_id}/tickets")]
71pub async fn list_building_tickets(
72 state: web::Data<AppState>,
73 building_id: web::Path<Uuid>,
74) -> impl Responder {
75 match state
76 .ticket_use_cases
77 .list_tickets_by_building(*building_id)
78 .await
79 {
80 Ok(tickets) => HttpResponse::Ok().json(tickets),
81 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
82 }
83}
84
85#[get("/organizations/{organization_id}/tickets")]
86pub async fn list_organization_tickets(
87 state: web::Data<AppState>,
88 organization_id: web::Path<Uuid>,
89) -> impl Responder {
90 match state
91 .ticket_use_cases
92 .list_tickets_by_organization(*organization_id)
93 .await
94 {
95 Ok(tickets) => HttpResponse::Ok().json(tickets),
96 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
97 }
98}
99
100#[get("/tickets/my-tickets")]
101pub async fn list_my_tickets(
102 state: web::Data<AppState>,
103 user: AuthenticatedUser,
104) -> impl Responder {
105 let created_by = user.user_id;
106
107 match state.ticket_use_cases.list_my_tickets(created_by).await {
108 Ok(tickets) => HttpResponse::Ok().json(tickets),
109 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
110 }
111}
112
113#[get("/tickets/assigned-to-me")]
114pub async fn list_assigned_tickets(
115 state: web::Data<AppState>,
116 user: AuthenticatedUser,
117) -> impl Responder {
118 let assigned_to = user.user_id;
119
120 match state
121 .ticket_use_cases
122 .list_assigned_tickets(assigned_to)
123 .await
124 {
125 Ok(tickets) => HttpResponse::Ok().json(tickets),
126 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
127 }
128}
129
130#[get("/buildings/{building_id}/tickets/status/{status}")]
131pub async fn list_tickets_by_status(
132 state: web::Data<AppState>,
133 path: web::Path<(Uuid, String)>,
134) -> impl Responder {
135 let (building_id, status_str) = path.into_inner();
136
137 let status = match status_str.as_str() {
138 "open" => TicketStatus::Open,
139 "in_progress" => TicketStatus::InProgress,
140 "resolved" => TicketStatus::Resolved,
141 "closed" => TicketStatus::Closed,
142 "cancelled" => TicketStatus::Cancelled,
143 _ => {
144 return HttpResponse::BadRequest().json(serde_json::json!({
145 "error": format!("Invalid status: {}", status_str)
146 }))
147 }
148 };
149
150 match state
151 .ticket_use_cases
152 .list_tickets_by_status(building_id, status)
153 .await
154 {
155 Ok(tickets) => HttpResponse::Ok().json(tickets),
156 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
157 }
158}
159
160#[delete("/tickets/{id}")]
161pub async fn delete_ticket(
162 state: web::Data<AppState>,
163 user: AuthenticatedUser,
164 id: web::Path<Uuid>,
165) -> impl Responder {
166 let organization_id = match user.require_organization() {
167 Ok(org_id) => org_id,
168 Err(e) => {
169 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
170 }
171 };
172
173 match state.ticket_use_cases.delete_ticket(*id).await {
174 Ok(true) => {
175 AuditLogEntry::new(
176 AuditEventType::TicketDeleted,
177 Some(user.user_id),
178 Some(organization_id),
179 )
180 .with_resource("Ticket", *id)
181 .log();
182
183 HttpResponse::NoContent().finish()
184 }
185 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
186 "error": "Ticket not found"
187 })),
188 Err(err) => {
189 AuditLogEntry::new(
190 AuditEventType::TicketDeleted,
191 Some(user.user_id),
192 Some(organization_id),
193 )
194 .with_error(err.clone())
195 .log();
196
197 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
198 }
199 }
200}
201
202#[put("/tickets/{id}/assign")]
205pub async fn assign_ticket(
206 state: web::Data<AppState>,
207 user: AuthenticatedUser,
208 id: web::Path<Uuid>,
209 request: web::Json<AssignTicketRequest>,
210) -> impl Responder {
211 let organization_id = match user.require_organization() {
212 Ok(org_id) => org_id,
213 Err(e) => {
214 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
215 }
216 };
217
218 match state
219 .ticket_use_cases
220 .assign_ticket(*id, request.into_inner())
221 .await
222 {
223 Ok(ticket) => {
224 AuditLogEntry::new(
225 AuditEventType::TicketAssigned,
226 Some(user.user_id),
227 Some(organization_id),
228 )
229 .with_resource("Ticket", ticket.id)
230 .log();
231
232 HttpResponse::Ok().json(ticket)
233 }
234 Err(err) => {
235 AuditLogEntry::new(
236 AuditEventType::TicketAssigned,
237 Some(user.user_id),
238 Some(organization_id),
239 )
240 .with_error(err.clone())
241 .log();
242
243 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
244 }
245 }
246}
247
248#[put("/tickets/{id}/start-work")]
249pub async fn start_work(
250 state: web::Data<AppState>,
251 user: AuthenticatedUser,
252 id: web::Path<Uuid>,
253) -> impl Responder {
254 let organization_id = match user.require_organization() {
255 Ok(org_id) => org_id,
256 Err(e) => {
257 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
258 }
259 };
260
261 match state.ticket_use_cases.start_work(*id).await {
262 Ok(ticket) => {
263 AuditLogEntry::new(
264 AuditEventType::TicketStatusChanged,
265 Some(user.user_id),
266 Some(organization_id),
267 )
268 .with_resource("Ticket", ticket.id)
269 .log();
270
271 HttpResponse::Ok().json(ticket)
272 }
273 Err(err) => {
274 AuditLogEntry::new(
275 AuditEventType::TicketStatusChanged,
276 Some(user.user_id),
277 Some(organization_id),
278 )
279 .with_error(err.clone())
280 .log();
281
282 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
283 }
284 }
285}
286
287#[put("/tickets/{id}/resolve")]
288pub async fn resolve_ticket(
289 state: web::Data<AppState>,
290 user: AuthenticatedUser,
291 id: web::Path<Uuid>,
292 request: web::Json<ResolveTicketRequest>,
293) -> impl Responder {
294 let organization_id = match user.require_organization() {
295 Ok(org_id) => org_id,
296 Err(e) => {
297 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
298 }
299 };
300
301 match state
302 .ticket_use_cases
303 .resolve_ticket(*id, request.into_inner())
304 .await
305 {
306 Ok(ticket) => {
307 AuditLogEntry::new(
308 AuditEventType::TicketResolved,
309 Some(user.user_id),
310 Some(organization_id),
311 )
312 .with_resource("Ticket", ticket.id)
313 .log();
314
315 HttpResponse::Ok().json(ticket)
316 }
317 Err(err) => {
318 AuditLogEntry::new(
319 AuditEventType::TicketResolved,
320 Some(user.user_id),
321 Some(organization_id),
322 )
323 .with_error(err.clone())
324 .log();
325
326 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
327 }
328 }
329}
330
331#[put("/tickets/{id}/close")]
332pub async fn close_ticket(
333 state: web::Data<AppState>,
334 user: AuthenticatedUser,
335 id: web::Path<Uuid>,
336) -> impl Responder {
337 let organization_id = match user.require_organization() {
338 Ok(org_id) => org_id,
339 Err(e) => {
340 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
341 }
342 };
343
344 match state.ticket_use_cases.close_ticket(*id).await {
345 Ok(ticket) => {
346 AuditLogEntry::new(
347 AuditEventType::TicketClosed,
348 Some(user.user_id),
349 Some(organization_id),
350 )
351 .with_resource("Ticket", ticket.id)
352 .log();
353
354 HttpResponse::Ok().json(ticket)
355 }
356 Err(err) => {
357 AuditLogEntry::new(
358 AuditEventType::TicketClosed,
359 Some(user.user_id),
360 Some(organization_id),
361 )
362 .with_error(err.clone())
363 .log();
364
365 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
366 }
367 }
368}
369
370#[put("/tickets/{id}/cancel")]
371pub async fn cancel_ticket(
372 state: web::Data<AppState>,
373 user: AuthenticatedUser,
374 id: web::Path<Uuid>,
375 request: web::Json<CancelTicketRequest>,
376) -> impl Responder {
377 let organization_id = match user.require_organization() {
378 Ok(org_id) => org_id,
379 Err(e) => {
380 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
381 }
382 };
383
384 match state
385 .ticket_use_cases
386 .cancel_ticket(*id, request.into_inner())
387 .await
388 {
389 Ok(ticket) => {
390 AuditLogEntry::new(
391 AuditEventType::TicketCancelled,
392 Some(user.user_id),
393 Some(organization_id),
394 )
395 .with_resource("Ticket", ticket.id)
396 .log();
397
398 HttpResponse::Ok().json(ticket)
399 }
400 Err(err) => {
401 AuditLogEntry::new(
402 AuditEventType::TicketCancelled,
403 Some(user.user_id),
404 Some(organization_id),
405 )
406 .with_error(err.clone())
407 .log();
408
409 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
410 }
411 }
412}
413
414#[put("/tickets/{id}/reopen")]
415pub async fn reopen_ticket(
416 state: web::Data<AppState>,
417 user: AuthenticatedUser,
418 id: web::Path<Uuid>,
419 request: web::Json<ReopenTicketRequest>,
420) -> impl Responder {
421 let organization_id = match user.require_organization() {
422 Ok(org_id) => org_id,
423 Err(e) => {
424 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
425 }
426 };
427
428 match state
429 .ticket_use_cases
430 .reopen_ticket(*id, request.into_inner())
431 .await
432 {
433 Ok(ticket) => {
434 AuditLogEntry::new(
435 AuditEventType::TicketReopened,
436 Some(user.user_id),
437 Some(organization_id),
438 )
439 .with_resource("Ticket", ticket.id)
440 .log();
441
442 HttpResponse::Ok().json(ticket)
443 }
444 Err(err) => {
445 AuditLogEntry::new(
446 AuditEventType::TicketReopened,
447 Some(user.user_id),
448 Some(organization_id),
449 )
450 .with_error(err.clone())
451 .log();
452
453 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
454 }
455 }
456}
457
458#[get("/buildings/{building_id}/tickets/statistics")]
461pub async fn get_ticket_statistics(
462 state: web::Data<AppState>,
463 building_id: web::Path<Uuid>,
464) -> impl Responder {
465 match state
466 .ticket_use_cases
467 .get_ticket_statistics(*building_id)
468 .await
469 {
470 Ok(stats) => HttpResponse::Ok().json(stats),
471 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
472 }
473}
474
475#[get("/buildings/{building_id}/tickets/overdue")]
476pub async fn get_overdue_tickets(
477 state: web::Data<AppState>,
478 building_id: web::Path<Uuid>,
479 query: web::Query<OverdueQuery>,
480) -> impl Responder {
481 let max_days = query.max_days.unwrap_or(7); match state
484 .ticket_use_cases
485 .get_overdue_tickets(*building_id, max_days)
486 .await
487 {
488 Ok(tickets) => HttpResponse::Ok().json(tickets),
489 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
490 }
491}
492
493#[derive(serde::Deserialize)]
494pub struct OverdueQuery {
495 pub max_days: Option<i64>,
496}