1use crate::application::dto::{CreateResourceBookingDto, UpdateResourceBookingDto};
2use crate::domain::entities::{BookingStatus, ResourceType};
3use crate::infrastructure::web::app_state::AppState;
4use crate::infrastructure::web::classification_erreurs;
5use crate::infrastructure::web::middleware::scope_guard::{
6 verify_booking_org_access, verify_building_org_access,
7};
8use crate::infrastructure::web::middleware::AuthenticatedUser;
9use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
10use chrono::{DateTime, Utc};
11use serde::Deserialize;
12use uuid::Uuid;
13
14#[post("/resource-bookings")]
35pub async fn create_booking(
36 data: web::Data<AppState>,
37 auth: AuthenticatedUser,
38 request: web::Json<CreateResourceBookingDto>,
39) -> impl Responder {
40 let org_id = match auth.require_organization() {
41 Ok(id) => id,
42 Err(e) => {
43 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
44 }
45 };
46 let is_syndic = auth.is_superadmin() || auth.role == "syndic";
49 match data
50 .resource_booking_use_cases
51 .create_booking(auth.user_id, org_id, is_syndic, request.into_inner())
52 .await
53 {
54 Ok(booking) => HttpResponse::Created().json(booking),
55 Err(e) => {
56 if e.contains("conflicts with") {
63 HttpResponse::Conflict().json(serde_json::json!({"error": e}))
64 } else if classification_erreurs::est_motif_acp_manquant(&e) {
65 HttpResponse::UnprocessableEntity().json(serde_json::json!({
66 "error": e,
67 "kind": "reservation_motif_required",
68 }))
69 } else if classification_erreurs::est_refus_owner_requis(&e) {
70 HttpResponse::Forbidden().json(serde_json::json!({
71 "error": e,
72 "kind": "owner_profile_required",
73 }))
74 } else if classification_erreurs::est_interdit(&e) {
75 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
76 } else if classification_erreurs::est_introuvable(&e) {
77 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
78 } else {
79 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
80 }
81 }
82 }
83}
84
85#[get("/resource-bookings/{id}")]
93pub async fn get_booking(
94 data: web::Data<AppState>,
95 auth: AuthenticatedUser,
96 id: web::Path<Uuid>,
97) -> impl Responder {
98 if let Err(err) = verify_booking_org_access(
103 &auth,
104 *id,
105 &data.resource_booking_use_cases,
106 &data.building_use_cases,
107 &data.acp_use_cases,
108 )
109 .await
110 {
111 return err.error_response();
112 }
113
114 match data
115 .resource_booking_use_cases
116 .get_booking(id.into_inner())
117 .await
118 {
119 Ok(booking) => HttpResponse::Ok().json(booking),
120 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
121 }
122}
123
124#[get("/buildings/{building_id}/resource-bookings")]
131pub async fn list_building_bookings(
132 data: web::Data<AppState>,
133 auth: AuthenticatedUser,
134 building_id: web::Path<Uuid>,
135) -> impl Responder {
136 if let Err(err) = verify_building_org_access(
141 &auth,
142 *building_id,
143 &data.building_use_cases,
144 &data.acp_use_cases,
145 )
146 .await
147 {
148 return err.error_response();
149 }
150
151 match data
152 .resource_booking_use_cases
153 .list_building_bookings(building_id.into_inner())
154 .await
155 {
156 Ok(bookings) => HttpResponse::Ok().json(bookings),
157 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
158 }
159}
160
161#[get("/buildings/{building_id}/resource-bookings/type/{resource_type}")]
168pub async fn list_by_resource_type(
169 data: web::Data<AppState>,
170 auth: AuthenticatedUser,
171 path: web::Path<(Uuid, String)>,
172) -> impl Responder {
173 if let Err(err) =
178 verify_building_org_access(&auth, path.0, &data.building_use_cases, &data.acp_use_cases)
179 .await
180 {
181 return err.error_response();
182 }
183
184 let (building_id, resource_type_str) = path.into_inner();
185
186 let resource_type: ResourceType =
188 match serde_json::from_str(&format!("\"{}\"", resource_type_str)) {
189 Ok(rt) => rt,
190 Err(_) => {
191 return HttpResponse::BadRequest().json(serde_json::json!({
192 "error": format!("Invalid resource type: {}", resource_type_str)
193 }))
194 }
195 };
196
197 match data
198 .resource_booking_use_cases
199 .list_by_resource_type(building_id, resource_type)
200 .await
201 {
202 Ok(bookings) => HttpResponse::Ok().json(bookings),
203 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
204 }
205}
206
207#[get("/buildings/{building_id}/resource-bookings/resource/{resource_type}/{resource_name}")]
214pub async fn list_by_resource(
215 data: web::Data<AppState>,
216 auth: AuthenticatedUser,
217 path: web::Path<(Uuid, String, String)>,
218) -> impl Responder {
219 if let Err(err) =
224 verify_building_org_access(&auth, path.0, &data.building_use_cases, &data.acp_use_cases)
225 .await
226 {
227 return err.error_response();
228 }
229
230 let (building_id, resource_type_str, resource_name) = path.into_inner();
231
232 let resource_type: ResourceType =
234 match serde_json::from_str(&format!("\"{}\"", resource_type_str)) {
235 Ok(rt) => rt,
236 Err(_) => {
237 return HttpResponse::BadRequest().json(serde_json::json!({
238 "error": format!("Invalid resource type: {}", resource_type_str)
239 }))
240 }
241 };
242
243 match data
244 .resource_booking_use_cases
245 .list_by_resource(building_id, resource_type, resource_name)
246 .await
247 {
248 Ok(bookings) => HttpResponse::Ok().json(bookings),
249 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
250 }
251}
252
253#[get("/resource-bookings/my")]
260pub async fn list_my_bookings(
261 data: web::Data<AppState>,
262 auth: AuthenticatedUser,
263) -> impl Responder {
264 let org_id = match auth.require_organization() {
265 Ok(id) => id,
266 Err(e) => {
267 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
268 }
269 };
270 match data
271 .resource_booking_use_cases
272 .list_user_bookings(auth.user_id, org_id)
273 .await
274 {
275 Ok(bookings) => HttpResponse::Ok().json(bookings),
276 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
277 }
278}
279
280#[get("/resource-bookings/my/status/{status}")]
287pub async fn list_my_bookings_by_status(
288 data: web::Data<AppState>,
289 auth: AuthenticatedUser,
290 status: web::Path<String>,
291) -> impl Responder {
292 let booking_status: BookingStatus =
294 match serde_json::from_str(&format!("\"{}\"", status.into_inner())) {
295 Ok(s) => s,
296 Err(_) => {
297 return HttpResponse::BadRequest()
298 .json(serde_json::json!({"error": "Invalid status"}))
299 }
300 };
301
302 let org_id = match auth.require_organization() {
303 Ok(id) => id,
304 Err(e) => {
305 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
306 }
307 };
308 match data
309 .resource_booking_use_cases
310 .list_user_bookings_by_status(auth.user_id, org_id, booking_status)
311 .await
312 {
313 Ok(bookings) => HttpResponse::Ok().json(bookings),
314 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
315 }
316}
317
318#[get("/buildings/{building_id}/resource-bookings/status/{status}")]
325pub async fn list_building_bookings_by_status(
326 data: web::Data<AppState>,
327 auth: AuthenticatedUser,
328 path: web::Path<(Uuid, String)>,
329) -> impl Responder {
330 if let Err(err) =
335 verify_building_org_access(&auth, path.0, &data.building_use_cases, &data.acp_use_cases)
336 .await
337 {
338 return err.error_response();
339 }
340
341 let (building_id, status_str) = path.into_inner();
342
343 let booking_status: BookingStatus = match serde_json::from_str(&format!("\"{}\"", status_str)) {
345 Ok(s) => s,
346 Err(_) => {
347 return HttpResponse::BadRequest().json(serde_json::json!({
348 "error": format!("Invalid status: {}", status_str)
349 }))
350 }
351 };
352
353 match data
354 .resource_booking_use_cases
355 .list_building_bookings_by_status(building_id, booking_status)
356 .await
357 {
358 Ok(bookings) => HttpResponse::Ok().json(bookings),
359 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
360 }
361}
362
363#[derive(Deserialize)]
364pub struct UpcomingQuery {
365 limit: Option<i64>,
366}
367
368#[get("/buildings/{building_id}/resource-bookings/upcoming")]
375pub async fn list_upcoming_bookings(
376 data: web::Data<AppState>,
377 auth: AuthenticatedUser,
378 building_id: web::Path<Uuid>,
379 query: web::Query<UpcomingQuery>,
380) -> impl Responder {
381 if let Err(err) = verify_building_org_access(
386 &auth,
387 *building_id,
388 &data.building_use_cases,
389 &data.acp_use_cases,
390 )
391 .await
392 {
393 return err.error_response();
394 }
395
396 match data
397 .resource_booking_use_cases
398 .list_upcoming_bookings(building_id.into_inner(), query.limit)
399 .await
400 {
401 Ok(bookings) => HttpResponse::Ok().json(bookings),
402 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
403 }
404}
405
406#[get("/buildings/{building_id}/resource-bookings/active")]
413pub async fn list_active_bookings(
414 data: web::Data<AppState>,
415 auth: AuthenticatedUser,
416 building_id: web::Path<Uuid>,
417) -> impl Responder {
418 if let Err(err) = verify_building_org_access(
423 &auth,
424 *building_id,
425 &data.building_use_cases,
426 &data.acp_use_cases,
427 )
428 .await
429 {
430 return err.error_response();
431 }
432
433 match data
434 .resource_booking_use_cases
435 .list_active_bookings(building_id.into_inner())
436 .await
437 {
438 Ok(bookings) => HttpResponse::Ok().json(bookings),
439 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
440 }
441}
442
443#[derive(Deserialize)]
444pub struct PastQuery {
445 limit: Option<i64>,
446}
447
448#[get("/buildings/{building_id}/resource-bookings/past")]
455pub async fn list_past_bookings(
456 data: web::Data<AppState>,
457 auth: AuthenticatedUser,
458 building_id: web::Path<Uuid>,
459 query: web::Query<PastQuery>,
460) -> impl Responder {
461 if let Err(err) = verify_building_org_access(
466 &auth,
467 *building_id,
468 &data.building_use_cases,
469 &data.acp_use_cases,
470 )
471 .await
472 {
473 return err.error_response();
474 }
475
476 match data
477 .resource_booking_use_cases
478 .list_past_bookings(building_id.into_inner(), query.limit)
479 .await
480 {
481 Ok(bookings) => HttpResponse::Ok().json(bookings),
482 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
483 }
484}
485
486#[put("/resource-bookings/{id}")]
500pub async fn update_booking(
501 data: web::Data<AppState>,
502 auth: AuthenticatedUser,
503 id: web::Path<Uuid>,
504 request: web::Json<UpdateResourceBookingDto>,
505) -> impl Responder {
506 let org_id = match auth.require_organization() {
507 Ok(id) => id,
508 Err(e) => {
509 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
510 }
511 };
512 match data
513 .resource_booking_use_cases
514 .update_booking(id.into_inner(), auth.user_id, org_id, request.into_inner())
515 .await
516 {
517 Ok(booking) => HttpResponse::Ok().json(booking),
518 Err(e) => {
519 if e.contains("Only the booking owner") {
520 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
521 } else if classification_erreurs::est_introuvable(&e) {
522 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
523 } else {
524 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
525 }
526 }
527 }
528}
529
530#[post("/resource-bookings/{id}/cancel")]
540pub async fn cancel_booking(
541 data: web::Data<AppState>,
542 auth: AuthenticatedUser,
543 id: web::Path<Uuid>,
544) -> impl Responder {
545 let org_id = match auth.require_organization() {
546 Ok(id) => id,
547 Err(e) => {
548 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
549 }
550 };
551 match data
552 .resource_booking_use_cases
553 .cancel_booking(id.into_inner(), auth.user_id, org_id)
554 .await
555 {
556 Ok(booking) => HttpResponse::Ok().json(booking),
557 Err(e) => {
558 if e.contains("Only the booking owner") {
559 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
560 } else if classification_erreurs::est_introuvable(&e) {
561 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
562 } else {
563 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
564 }
565 }
566 }
567}
568
569#[post("/resource-bookings/{id}/complete")]
578pub async fn complete_booking(
579 data: web::Data<AppState>,
580 auth: AuthenticatedUser,
581 id: web::Path<Uuid>,
582) -> impl Responder {
583 if let Err(err) = verify_booking_org_access(
588 &auth,
589 *id,
590 &data.resource_booking_use_cases,
591 &data.building_use_cases,
592 &data.acp_use_cases,
593 )
594 .await
595 {
596 return err.error_response();
597 }
598
599 match data
600 .resource_booking_use_cases
601 .complete_booking(id.into_inner())
602 .await
603 {
604 Ok(booking) => HttpResponse::Ok().json(booking),
605 Err(e) => {
606 if classification_erreurs::est_introuvable(&e) {
607 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
608 } else {
609 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
610 }
611 }
612 }
613}
614
615#[post("/resource-bookings/{id}/no-show")]
624pub async fn mark_no_show(
625 data: web::Data<AppState>,
626 auth: AuthenticatedUser,
627 id: web::Path<Uuid>,
628) -> impl Responder {
629 if let Err(err) = verify_booking_org_access(
634 &auth,
635 *id,
636 &data.resource_booking_use_cases,
637 &data.building_use_cases,
638 &data.acp_use_cases,
639 )
640 .await
641 {
642 return err.error_response();
643 }
644
645 match data
646 .resource_booking_use_cases
647 .mark_no_show(id.into_inner())
648 .await
649 {
650 Ok(booking) => HttpResponse::Ok().json(booking),
651 Err(e) => {
652 if classification_erreurs::est_introuvable(&e) {
653 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
654 } else {
655 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
656 }
657 }
658 }
659}
660
661#[post("/resource-bookings/{id}/confirm")]
670pub async fn confirm_booking(
671 data: web::Data<AppState>,
672 auth: AuthenticatedUser,
673 id: web::Path<Uuid>,
674) -> impl Responder {
675 if let Err(err) = verify_booking_org_access(
680 &auth,
681 *id,
682 &data.resource_booking_use_cases,
683 &data.building_use_cases,
684 &data.acp_use_cases,
685 )
686 .await
687 {
688 return err.error_response();
689 }
690
691 match data
692 .resource_booking_use_cases
693 .confirm_booking(id.into_inner())
694 .await
695 {
696 Ok(booking) => HttpResponse::Ok().json(booking),
697 Err(e) => {
698 if classification_erreurs::est_introuvable(&e) {
699 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
700 } else {
701 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
702 }
703 }
704 }
705}
706
707#[delete("/resource-bookings/{id}")]
716pub async fn delete_booking(
717 data: web::Data<AppState>,
718 auth: AuthenticatedUser,
719 id: web::Path<Uuid>,
720) -> impl Responder {
721 let org_id = match auth.require_organization() {
722 Ok(id) => id,
723 Err(e) => {
724 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
725 }
726 };
727 match data
728 .resource_booking_use_cases
729 .delete_booking(id.into_inner(), auth.user_id, org_id)
730 .await
731 {
732 Ok(()) => HttpResponse::NoContent().finish(),
733 Err(e) => {
734 if e.contains("Only the booking owner") {
735 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
736 } else if classification_erreurs::est_introuvable(&e) {
737 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
738 } else {
739 HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
740 }
741 }
742 }
743}
744
745#[derive(Deserialize)]
746pub struct CheckConflictsQuery {
747 pub building_id: Uuid,
748 pub resource_type: String,
749 pub resource_name: String,
750 pub start_time: DateTime<Utc>,
751 pub end_time: DateTime<Utc>,
752 pub exclude_booking_id: Option<Uuid>,
753}
754
755#[get("/resource-bookings/check-conflicts")]
770pub async fn check_conflicts(
771 data: web::Data<AppState>,
772 auth: AuthenticatedUser,
773 query: web::Query<CheckConflictsQuery>,
774) -> impl Responder {
775 if let Err(err) = verify_building_org_access(
780 &auth,
781 query.building_id,
782 &data.building_use_cases,
783 &data.acp_use_cases,
784 )
785 .await
786 {
787 return err.error_response();
788 }
789
790 let resource_type: ResourceType =
792 match serde_json::from_str(&format!("\"{}\"", query.resource_type)) {
793 Ok(rt) => rt,
794 Err(_) => {
795 return HttpResponse::BadRequest().json(serde_json::json!({
796 "error": format!("Invalid resource type: {}", query.resource_type)
797 }))
798 }
799 };
800
801 match data
802 .resource_booking_use_cases
803 .check_conflicts(
804 query.building_id,
805 resource_type,
806 query.resource_name.clone(),
807 query.start_time,
808 query.end_time,
809 query.exclude_booking_id,
810 )
811 .await
812 {
813 Ok(conflicts) => HttpResponse::Ok().json(conflicts),
814 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
815 }
816}
817
818#[get("/buildings/{building_id}/resource-bookings/statistics")]
825pub async fn get_booking_statistics(
826 data: web::Data<AppState>,
827 auth: AuthenticatedUser,
828 building_id: web::Path<Uuid>,
829) -> impl Responder {
830 if let Err(err) = verify_building_org_access(
835 &auth,
836 *building_id,
837 &data.building_use_cases,
838 &data.acp_use_cases,
839 )
840 .await
841 {
842 return err.error_response();
843 }
844
845 match data
846 .resource_booking_use_cases
847 .get_statistics(building_id.into_inner())
848 .await
849 {
850 Ok(stats) => HttpResponse::Ok().json(stats),
851 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
852 }
853}