koprogo_api/infrastructure/web/handlers/
resource_booking_handlers.rs

1use crate::application::dto::{CreateResourceBookingDto, UpdateResourceBookingDto};
2use crate::domain::entities::{BookingStatus, ResourceType};
3use crate::infrastructure::web::app_state::AppState;
4use crate::infrastructure::web::middleware::AuthenticatedUser;
5use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
6use chrono::{DateTime, Utc};
7use serde::Deserialize;
8use uuid::Uuid;
9
10/// Create a new resource booking
11///
12/// POST /resource-bookings
13///
14/// # Request Body
15/// - building_id: UUID
16/// - resource_type: ResourceType (MeetingRoom, LaundryRoom, Gym, etc.)
17/// - resource_name: String (e.g., "Meeting Room A")
18/// - start_time: DateTime<Utc>
19/// - end_time: DateTime<Utc>
20/// - notes: Option<String>
21/// - recurring_pattern: RecurringPattern (default: None)
22/// - recurrence_end_date: Option<DateTime<Utc>>
23/// - max_duration_hours: Option<i64> (default: 4)
24/// - max_advance_days: Option<i64> (default: 30)
25///
26/// # Responses
27/// - 201 Created: Booking created successfully
28/// - 400 Bad Request: Validation error or conflict
29/// - 404 Not Found: Building not found
30#[post("/resource-bookings")]
31pub async fn create_booking(
32    data: web::Data<AppState>,
33    auth: AuthenticatedUser,
34    request: web::Json<CreateResourceBookingDto>,
35) -> impl Responder {
36    match data
37        .resource_booking_use_cases
38        .create_booking(auth.user_id, request.into_inner())
39        .await
40    {
41        Ok(booking) => HttpResponse::Created().json(booking),
42        Err(e) => {
43            if e.contains("conflicts with") {
44                HttpResponse::Conflict().json(serde_json::json!({"error": e}))
45            } else if e.contains("not found") {
46                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
47            } else {
48                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
49            }
50        }
51    }
52}
53
54/// Get booking by ID
55///
56/// GET /resource-bookings/:id
57///
58/// # Responses
59/// - 200 OK: Booking details
60/// - 404 Not Found: Booking not found
61#[get("/resource-bookings/{id}")]
62pub async fn get_booking(
63    data: web::Data<AppState>,
64    _auth: AuthenticatedUser,
65    id: web::Path<Uuid>,
66) -> impl Responder {
67    match data
68        .resource_booking_use_cases
69        .get_booking(id.into_inner())
70        .await
71    {
72        Ok(booking) => HttpResponse::Ok().json(booking),
73        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
74    }
75}
76
77/// List all bookings for a building
78///
79/// GET /buildings/:building_id/resource-bookings
80///
81/// # Responses
82/// - 200 OK: List of bookings
83#[get("/buildings/{building_id}/resource-bookings")]
84pub async fn list_building_bookings(
85    data: web::Data<AppState>,
86    _auth: AuthenticatedUser,
87    building_id: web::Path<Uuid>,
88) -> impl Responder {
89    match data
90        .resource_booking_use_cases
91        .list_building_bookings(building_id.into_inner())
92        .await
93    {
94        Ok(bookings) => HttpResponse::Ok().json(bookings),
95        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
96    }
97}
98
99/// List bookings by resource type
100///
101/// GET /buildings/:building_id/resource-bookings/type/:resource_type
102///
103/// # Responses
104/// - 200 OK: List of bookings for resource type
105#[get("/buildings/{building_id}/resource-bookings/type/{resource_type}")]
106pub async fn list_by_resource_type(
107    data: web::Data<AppState>,
108    _auth: AuthenticatedUser,
109    path: web::Path<(Uuid, String)>,
110) -> impl Responder {
111    let (building_id, resource_type_str) = path.into_inner();
112
113    // Parse resource_type from string
114    let resource_type: ResourceType =
115        match serde_json::from_str(&format!("\"{}\"", resource_type_str)) {
116            Ok(rt) => rt,
117            Err(_) => {
118                return HttpResponse::BadRequest().json(serde_json::json!({
119                    "error": format!("Invalid resource type: {}", resource_type_str)
120                }))
121            }
122        };
123
124    match data
125        .resource_booking_use_cases
126        .list_by_resource_type(building_id, resource_type)
127        .await
128    {
129        Ok(bookings) => HttpResponse::Ok().json(bookings),
130        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
131    }
132}
133
134/// List bookings for a specific resource
135///
136/// GET /buildings/:building_id/resource-bookings/resource/:resource_type/:resource_name
137///
138/// # Responses
139/// - 200 OK: List of bookings for specific resource
140#[get("/buildings/{building_id}/resource-bookings/resource/{resource_type}/{resource_name}")]
141pub async fn list_by_resource(
142    data: web::Data<AppState>,
143    _auth: AuthenticatedUser,
144    path: web::Path<(Uuid, String, String)>,
145) -> impl Responder {
146    let (building_id, resource_type_str, resource_name) = path.into_inner();
147
148    // Parse resource_type from string
149    let resource_type: ResourceType =
150        match serde_json::from_str(&format!("\"{}\"", resource_type_str)) {
151            Ok(rt) => rt,
152            Err(_) => {
153                return HttpResponse::BadRequest().json(serde_json::json!({
154                    "error": format!("Invalid resource type: {}", resource_type_str)
155                }))
156            }
157        };
158
159    match data
160        .resource_booking_use_cases
161        .list_by_resource(building_id, resource_type, resource_name)
162        .await
163    {
164        Ok(bookings) => HttpResponse::Ok().json(bookings),
165        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
166    }
167}
168
169/// List user's bookings
170///
171/// GET /resource-bookings/my
172///
173/// # Responses
174/// - 200 OK: List of user's bookings
175#[get("/resource-bookings/my")]
176pub async fn list_my_bookings(
177    data: web::Data<AppState>,
178    auth: AuthenticatedUser,
179) -> impl Responder {
180    match data
181        .resource_booking_use_cases
182        .list_user_bookings(auth.user_id)
183        .await
184    {
185        Ok(bookings) => HttpResponse::Ok().json(bookings),
186        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
187    }
188}
189
190/// List user's bookings by status
191///
192/// GET /resource-bookings/my/status/:status
193///
194/// # Responses
195/// - 200 OK: List of user's bookings with status
196#[get("/resource-bookings/my/status/{status}")]
197pub async fn list_my_bookings_by_status(
198    data: web::Data<AppState>,
199    auth: AuthenticatedUser,
200    status: web::Path<String>,
201) -> impl Responder {
202    // Parse status from string
203    let booking_status: BookingStatus =
204        match serde_json::from_str(&format!("\"{}\"", status.into_inner())) {
205            Ok(s) => s,
206            Err(_) => {
207                return HttpResponse::BadRequest()
208                    .json(serde_json::json!({"error": "Invalid status"}))
209            }
210        };
211
212    match data
213        .resource_booking_use_cases
214        .list_user_bookings_by_status(auth.user_id, booking_status)
215        .await
216    {
217        Ok(bookings) => HttpResponse::Ok().json(bookings),
218        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
219    }
220}
221
222/// List building bookings by status
223///
224/// GET /buildings/:building_id/resource-bookings/status/:status
225///
226/// # Responses
227/// - 200 OK: List of bookings with status
228#[get("/buildings/{building_id}/resource-bookings/status/{status}")]
229pub async fn list_building_bookings_by_status(
230    data: web::Data<AppState>,
231    _auth: AuthenticatedUser,
232    path: web::Path<(Uuid, String)>,
233) -> impl Responder {
234    let (building_id, status_str) = path.into_inner();
235
236    // Parse status from string
237    let booking_status: BookingStatus = match serde_json::from_str(&format!("\"{}\"", status_str)) {
238        Ok(s) => s,
239        Err(_) => {
240            return HttpResponse::BadRequest().json(serde_json::json!({
241                "error": format!("Invalid status: {}", status_str)
242            }))
243        }
244    };
245
246    match data
247        .resource_booking_use_cases
248        .list_building_bookings_by_status(building_id, booking_status)
249        .await
250    {
251        Ok(bookings) => HttpResponse::Ok().json(bookings),
252        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
253    }
254}
255
256#[derive(Deserialize)]
257pub struct UpcomingQuery {
258    limit: Option<i64>,
259}
260
261/// List upcoming bookings (future, confirmed or pending)
262///
263/// GET /buildings/:building_id/resource-bookings/upcoming?limit=50
264///
265/// # Responses
266/// - 200 OK: List of upcoming bookings
267#[get("/buildings/{building_id}/resource-bookings/upcoming")]
268pub async fn list_upcoming_bookings(
269    data: web::Data<AppState>,
270    _auth: AuthenticatedUser,
271    building_id: web::Path<Uuid>,
272    query: web::Query<UpcomingQuery>,
273) -> impl Responder {
274    match data
275        .resource_booking_use_cases
276        .list_upcoming_bookings(building_id.into_inner(), query.limit)
277        .await
278    {
279        Ok(bookings) => HttpResponse::Ok().json(bookings),
280        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
281    }
282}
283
284/// List active bookings (currently in progress)
285///
286/// GET /buildings/:building_id/resource-bookings/active
287///
288/// # Responses
289/// - 200 OK: List of active bookings
290#[get("/buildings/{building_id}/resource-bookings/active")]
291pub async fn list_active_bookings(
292    data: web::Data<AppState>,
293    _auth: AuthenticatedUser,
294    building_id: web::Path<Uuid>,
295) -> impl Responder {
296    match data
297        .resource_booking_use_cases
298        .list_active_bookings(building_id.into_inner())
299        .await
300    {
301        Ok(bookings) => HttpResponse::Ok().json(bookings),
302        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
303    }
304}
305
306#[derive(Deserialize)]
307pub struct PastQuery {
308    limit: Option<i64>,
309}
310
311/// List past bookings
312///
313/// GET /buildings/:building_id/resource-bookings/past?limit=50
314///
315/// # Responses
316/// - 200 OK: List of past bookings
317#[get("/buildings/{building_id}/resource-bookings/past")]
318pub async fn list_past_bookings(
319    data: web::Data<AppState>,
320    _auth: AuthenticatedUser,
321    building_id: web::Path<Uuid>,
322    query: web::Query<PastQuery>,
323) -> impl Responder {
324    match data
325        .resource_booking_use_cases
326        .list_past_bookings(building_id.into_inner(), query.limit)
327        .await
328    {
329        Ok(bookings) => HttpResponse::Ok().json(bookings),
330        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
331    }
332}
333
334/// Update booking details (resource_name, notes)
335///
336/// PUT /resource-bookings/:id
337///
338/// # Request Body
339/// - resource_name: Option<String>
340/// - notes: Option<String>
341///
342/// # Responses
343/// - 200 OK: Booking updated
344/// - 400 Bad Request: Validation error
345/// - 403 Forbidden: Not booking owner
346/// - 404 Not Found: Booking not found
347#[put("/resource-bookings/{id}")]
348pub async fn update_booking(
349    data: web::Data<AppState>,
350    auth: AuthenticatedUser,
351    id: web::Path<Uuid>,
352    request: web::Json<UpdateResourceBookingDto>,
353) -> impl Responder {
354    match data
355        .resource_booking_use_cases
356        .update_booking(id.into_inner(), auth.user_id, request.into_inner())
357        .await
358    {
359        Ok(booking) => HttpResponse::Ok().json(booking),
360        Err(e) => {
361            if e.contains("Only the booking owner") {
362                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
363            } else if e.contains("not found") {
364                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
365            } else {
366                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
367            }
368        }
369    }
370}
371
372/// Cancel a booking
373///
374/// POST /resource-bookings/:id/cancel
375///
376/// # Responses
377/// - 200 OK: Booking cancelled
378/// - 400 Bad Request: Cannot cancel (invalid state)
379/// - 403 Forbidden: Not booking owner
380/// - 404 Not Found: Booking not found
381#[post("/resource-bookings/{id}/cancel")]
382pub async fn cancel_booking(
383    data: web::Data<AppState>,
384    auth: AuthenticatedUser,
385    id: web::Path<Uuid>,
386) -> impl Responder {
387    match data
388        .resource_booking_use_cases
389        .cancel_booking(id.into_inner(), auth.user_id)
390        .await
391    {
392        Ok(booking) => HttpResponse::Ok().json(booking),
393        Err(e) => {
394            if e.contains("Only the booking owner") {
395                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
396            } else if e.contains("not found") {
397                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
398            } else {
399                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
400            }
401        }
402    }
403}
404
405/// Complete a booking (admin only)
406///
407/// POST /resource-bookings/:id/complete
408///
409/// # Responses
410/// - 200 OK: Booking completed
411/// - 400 Bad Request: Cannot complete
412/// - 404 Not Found: Booking not found
413#[post("/resource-bookings/{id}/complete")]
414pub async fn complete_booking(
415    data: web::Data<AppState>,
416    _auth: AuthenticatedUser,
417    id: web::Path<Uuid>,
418) -> impl Responder {
419    match data
420        .resource_booking_use_cases
421        .complete_booking(id.into_inner())
422        .await
423    {
424        Ok(booking) => HttpResponse::Ok().json(booking),
425        Err(e) => {
426            if e.contains("not found") {
427                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
428            } else {
429                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
430            }
431        }
432    }
433}
434
435/// Mark booking as no-show (admin only)
436///
437/// POST /resource-bookings/:id/no-show
438///
439/// # Responses
440/// - 200 OK: Booking marked as no-show
441/// - 400 Bad Request: Cannot mark as no-show
442/// - 404 Not Found: Booking not found
443#[post("/resource-bookings/{id}/no-show")]
444pub async fn mark_no_show(
445    data: web::Data<AppState>,
446    _auth: AuthenticatedUser,
447    id: web::Path<Uuid>,
448) -> impl Responder {
449    match data
450        .resource_booking_use_cases
451        .mark_no_show(id.into_inner())
452        .await
453    {
454        Ok(booking) => HttpResponse::Ok().json(booking),
455        Err(e) => {
456            if e.contains("not found") {
457                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
458            } else {
459                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
460            }
461        }
462    }
463}
464
465/// Confirm a pending booking (admin only)
466///
467/// POST /resource-bookings/:id/confirm
468///
469/// # Responses
470/// - 200 OK: Booking confirmed
471/// - 400 Bad Request: Cannot confirm
472/// - 404 Not Found: Booking not found
473#[post("/resource-bookings/{id}/confirm")]
474pub async fn confirm_booking(
475    data: web::Data<AppState>,
476    _auth: AuthenticatedUser,
477    id: web::Path<Uuid>,
478) -> impl Responder {
479    match data
480        .resource_booking_use_cases
481        .confirm_booking(id.into_inner())
482        .await
483    {
484        Ok(booking) => HttpResponse::Ok().json(booking),
485        Err(e) => {
486            if e.contains("not found") {
487                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
488            } else {
489                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
490            }
491        }
492    }
493}
494
495/// Delete a booking
496///
497/// DELETE /resource-bookings/:id
498///
499/// # Responses
500/// - 204 No Content: Booking deleted
501/// - 403 Forbidden: Not booking owner
502/// - 404 Not Found: Booking not found
503#[delete("/resource-bookings/{id}")]
504pub async fn delete_booking(
505    data: web::Data<AppState>,
506    auth: AuthenticatedUser,
507    id: web::Path<Uuid>,
508) -> impl Responder {
509    match data
510        .resource_booking_use_cases
511        .delete_booking(id.into_inner(), auth.user_id)
512        .await
513    {
514        Ok(()) => HttpResponse::NoContent().finish(),
515        Err(e) => {
516            if e.contains("Only the booking owner") {
517                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
518            } else if e.contains("not found") {
519                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
520            } else {
521                HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
522            }
523        }
524    }
525}
526
527#[derive(Deserialize)]
528pub struct CheckConflictsQuery {
529    pub building_id: Uuid,
530    pub resource_type: String,
531    pub resource_name: String,
532    pub start_time: DateTime<Utc>,
533    pub end_time: DateTime<Utc>,
534    pub exclude_booking_id: Option<Uuid>,
535}
536
537/// Check for booking conflicts (preview before creating)
538///
539/// GET /resource-bookings/check-conflicts?building_id=...&resource_type=...&resource_name=...&start_time=...&end_time=...
540///
541/// # Query Parameters
542/// - building_id: UUID
543/// - resource_type: String
544/// - resource_name: String
545/// - start_time: ISO 8601 DateTime
546/// - end_time: ISO 8601 DateTime
547/// - exclude_booking_id: Option<UUID>
548///
549/// # Responses
550/// - 200 OK: List of conflicting bookings (empty if no conflicts)
551#[get("/resource-bookings/check-conflicts")]
552pub async fn check_conflicts(
553    data: web::Data<AppState>,
554    _auth: AuthenticatedUser,
555    query: web::Query<CheckConflictsQuery>,
556) -> impl Responder {
557    // Parse resource_type
558    let resource_type: ResourceType =
559        match serde_json::from_str(&format!("\"{}\"", query.resource_type)) {
560            Ok(rt) => rt,
561            Err(_) => {
562                return HttpResponse::BadRequest().json(serde_json::json!({
563                    "error": format!("Invalid resource type: {}", query.resource_type)
564                }))
565            }
566        };
567
568    match data
569        .resource_booking_use_cases
570        .check_conflicts(
571            query.building_id,
572            resource_type,
573            query.resource_name.clone(),
574            query.start_time,
575            query.end_time,
576            query.exclude_booking_id,
577        )
578        .await
579    {
580        Ok(conflicts) => HttpResponse::Ok().json(conflicts),
581        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
582    }
583}
584
585/// Get booking statistics for a building
586///
587/// GET /buildings/:building_id/resource-bookings/statistics
588///
589/// # Responses
590/// - 200 OK: Booking statistics
591#[get("/buildings/{building_id}/resource-bookings/statistics")]
592pub async fn get_booking_statistics(
593    data: web::Data<AppState>,
594    _auth: AuthenticatedUser,
595    building_id: web::Path<Uuid>,
596) -> impl Responder {
597    match data
598        .resource_booking_use_cases
599        .get_statistics(building_id.into_inner())
600        .await
601    {
602        Ok(stats) => HttpResponse::Ok().json(stats),
603        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
604    }
605}