koprogo_api/infrastructure/web/handlers/
ticket_handlers.rs

1use 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// ==================== Ticket CRUD Endpoints ====================
12
13#[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    // Use owner_id from AuthenticatedUser as created_by
27    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// ==================== Ticket Workflow Endpoints ====================
203
204#[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// ==================== Ticket Statistics Endpoints ====================
459
460#[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); // Default 7 days
482
483    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}