1use crate::application::dto::{
2 BookingStatisticsDto, CreateResourceBookingDto, ResourceBookingResponseDto,
3 UpdateResourceBookingDto,
4};
5use crate::application::ports::{OwnerRepository, ResourceBookingRepository};
6use crate::domain::entities::{BookingStatus, ResourceBooking, ResourceType};
7use chrono::Utc;
8use std::sync::Arc;
9use uuid::Uuid;
10
11pub struct ResourceBookingUseCases {
16 booking_repo: Arc<dyn ResourceBookingRepository>,
17 owner_repo: Arc<dyn OwnerRepository>,
18}
19
20impl ResourceBookingUseCases {
21 pub fn new(
22 booking_repo: Arc<dyn ResourceBookingRepository>,
23 owner_repo: Arc<dyn OwnerRepository>,
24 ) -> Self {
25 Self {
26 booking_repo,
27 owner_repo,
28 }
29 }
30
31 async fn resolve_owner(
33 &self,
34 user_id: Uuid,
35 organization_id: Uuid,
36 ) -> Result<crate::domain::entities::Owner, String> {
37 self.owner_repo
38 .find_by_user_id_and_organization(user_id, organization_id)
39 .await?
40 .ok_or_else(|| crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES.to_string())
57 }
58
59 pub async fn create_booking(
76 &self,
77 user_id: Uuid,
78 organization_id: Uuid,
79 is_syndic: bool,
80 dto: CreateResourceBookingDto,
81 ) -> Result<ResourceBookingResponseDto, String> {
82 if dto.on_behalf_of_acp {
83 return self
84 .create_booking_on_behalf_of_acp(user_id, is_syndic, dto)
85 .await;
86 }
87
88 let owner = self.resolve_owner(user_id, organization_id).await?;
89 let booked_by = owner.id;
90 let booking = ResourceBooking::new(
92 dto.building_id,
93 dto.resource_type.clone(),
94 dto.resource_name.clone(),
95 booked_by,
96 dto.start_time,
97 dto.end_time,
98 dto.notes.clone(),
99 dto.recurring_pattern.clone(),
100 dto.recurrence_end_date,
101 dto.max_duration_hours,
102 dto.max_advance_days,
103 )?;
104
105 let conflicts = self
107 .booking_repo
108 .find_conflicts(
109 dto.building_id,
110 dto.resource_type,
111 &dto.resource_name,
112 dto.start_time,
113 dto.end_time,
114 None, )
116 .await?;
117
118 if !conflicts.is_empty() {
119 return Err(format!(
120 "Booking conflicts with {} existing booking(s) for this resource",
121 conflicts.len()
122 ));
123 }
124
125 let created = self.booking_repo.create(&booking).await?;
127
128 self.enrich_booking_response(created).await
130 }
131
132 async fn create_booking_on_behalf_of_acp(
138 &self,
139 user_id: Uuid,
140 is_syndic: bool,
141 dto: CreateResourceBookingDto,
142 ) -> Result<ResourceBookingResponseDto, String> {
143 if !is_syndic {
144 return Err(crate::application::error::REFUS_ON_BEHALF_RESERVE_AUX_SYNDICS.to_string());
145 }
146
147 let motif = dto.motif.clone().unwrap_or_default();
148 let booking = ResourceBooking::new_on_behalf_of_acp(
149 dto.building_id,
150 dto.resource_type.clone(),
151 dto.resource_name.clone(),
152 user_id,
153 motif,
154 dto.start_time,
155 dto.end_time,
156 dto.notes.clone(),
157 dto.recurring_pattern.clone(),
158 dto.recurrence_end_date,
159 dto.max_duration_hours,
160 dto.max_advance_days,
161 )?;
162
163 let conflicts = self
164 .booking_repo
165 .find_conflicts(
166 dto.building_id,
167 dto.resource_type,
168 &dto.resource_name,
169 dto.start_time,
170 dto.end_time,
171 None,
172 )
173 .await?;
174
175 if !conflicts.is_empty() {
176 return Err(format!(
177 "Booking conflicts with {} existing booking(s) for this resource",
178 conflicts.len()
179 ));
180 }
181
182 let created = self.booking_repo.create(&booking).await?;
183
184 log::info!(
188 "reservation_on_behalf_acp: syndic_user_id={} building_id={} resource=\"{}\" motif=\"{}\"",
189 user_id,
190 dto.building_id,
191 dto.resource_name,
192 created.motif.clone().unwrap_or_default(),
193 );
194
195 self.enrich_booking_response(created).await
196 }
197
198 pub async fn get_booking(
200 &self,
201 booking_id: Uuid,
202 ) -> Result<ResourceBookingResponseDto, String> {
203 let booking = self
204 .booking_repo
205 .find_by_id(booking_id)
206 .await?
207 .ok_or("Booking not found".to_string())?;
208
209 self.enrich_booking_response(booking).await
210 }
211
212 pub async fn list_building_bookings(
214 &self,
215 building_id: Uuid,
216 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
217 let bookings = self.booking_repo.find_by_building(building_id).await?;
218 self.enrich_bookings_response(bookings).await
219 }
220
221 pub async fn list_by_resource_type(
223 &self,
224 building_id: Uuid,
225 resource_type: ResourceType,
226 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
227 let bookings = self
228 .booking_repo
229 .find_by_building_and_resource_type(building_id, resource_type)
230 .await?;
231 self.enrich_bookings_response(bookings).await
232 }
233
234 pub async fn list_by_resource(
236 &self,
237 building_id: Uuid,
238 resource_type: ResourceType,
239 resource_name: String,
240 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
241 let bookings = self
242 .booking_repo
243 .find_by_resource(building_id, resource_type, &resource_name)
244 .await?;
245 self.enrich_bookings_response(bookings).await
246 }
247
248 pub async fn list_user_bookings(
250 &self,
251 user_id: Uuid,
252 organization_id: Uuid,
253 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
254 let owner = self.resolve_owner(user_id, organization_id).await?;
255 let bookings = self.booking_repo.find_by_user(owner.id).await?;
256 self.enrich_bookings_response(bookings).await
257 }
258
259 pub async fn list_user_bookings_by_status(
261 &self,
262 user_id: Uuid,
263 organization_id: Uuid,
264 status: BookingStatus,
265 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
266 let owner = self.resolve_owner(user_id, organization_id).await?;
267 let bookings = self
268 .booking_repo
269 .find_by_user_and_status(owner.id, status)
270 .await?;
271 self.enrich_bookings_response(bookings).await
272 }
273
274 pub async fn list_building_bookings_by_status(
276 &self,
277 building_id: Uuid,
278 status: BookingStatus,
279 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
280 let bookings = self
281 .booking_repo
282 .find_by_building_and_status(building_id, status)
283 .await?;
284 self.enrich_bookings_response(bookings).await
285 }
286
287 pub async fn list_upcoming_bookings(
289 &self,
290 building_id: Uuid,
291 limit: Option<i64>,
292 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
293 let bookings = self.booking_repo.find_upcoming(building_id, limit).await?;
294 self.enrich_bookings_response(bookings).await
295 }
296
297 pub async fn list_active_bookings(
299 &self,
300 building_id: Uuid,
301 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
302 let bookings = self.booking_repo.find_active(building_id).await?;
303 self.enrich_bookings_response(bookings).await
304 }
305
306 pub async fn list_past_bookings(
308 &self,
309 building_id: Uuid,
310 limit: Option<i64>,
311 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
312 let bookings = self.booking_repo.find_past(building_id, limit).await?;
313 self.enrich_bookings_response(bookings).await
314 }
315
316 pub async fn update_booking(
323 &self,
324 booking_id: Uuid,
325 user_id: Uuid,
326 organization_id: Uuid,
327 dto: UpdateResourceBookingDto,
328 ) -> Result<ResourceBookingResponseDto, String> {
329 let owner = self.resolve_owner(user_id, organization_id).await?;
330 let mut booking = self
331 .booking_repo
332 .find_by_id(booking_id)
333 .await?
334 .ok_or("Booking not found".to_string())?;
335
336 if booking.booked_by != Some(owner.id) {
338 return Err("Only the booking owner can update this booking".to_string());
339 }
340
341 booking.update_details(dto.resource_name, dto.notes)?;
343
344 let updated = self.booking_repo.update(&booking).await?;
346
347 self.enrich_booking_response(updated).await
349 }
350
351 pub async fn cancel_booking(
356 &self,
357 booking_id: Uuid,
358 user_id: Uuid,
359 organization_id: Uuid,
360 ) -> Result<ResourceBookingResponseDto, String> {
361 let owner = self.resolve_owner(user_id, organization_id).await?;
362 let mut booking = self
363 .booking_repo
364 .find_by_id(booking_id)
365 .await?
366 .ok_or("Booking not found".to_string())?;
367
368 booking.cancel(owner.id)?;
370
371 let updated = self.booking_repo.update(&booking).await?;
373
374 self.enrich_booking_response(updated).await
376 }
377
378 pub async fn complete_booking(
383 &self,
384 booking_id: Uuid,
385 ) -> Result<ResourceBookingResponseDto, String> {
386 let mut booking = self
387 .booking_repo
388 .find_by_id(booking_id)
389 .await?
390 .ok_or("Booking not found".to_string())?;
391
392 booking.complete()?;
394
395 let updated = self.booking_repo.update(&booking).await?;
397
398 self.enrich_booking_response(updated).await
400 }
401
402 pub async fn mark_no_show(
407 &self,
408 booking_id: Uuid,
409 ) -> Result<ResourceBookingResponseDto, String> {
410 let mut booking = self
411 .booking_repo
412 .find_by_id(booking_id)
413 .await?
414 .ok_or("Booking not found".to_string())?;
415
416 booking.mark_no_show()?;
418
419 let updated = self.booking_repo.update(&booking).await?;
421
422 self.enrich_booking_response(updated).await
424 }
425
426 pub async fn confirm_booking(
431 &self,
432 booking_id: Uuid,
433 ) -> Result<ResourceBookingResponseDto, String> {
434 let mut booking = self
435 .booking_repo
436 .find_by_id(booking_id)
437 .await?
438 .ok_or("Booking not found".to_string())?;
439
440 booking.confirm()?;
442
443 let updated = self.booking_repo.update(&booking).await?;
445
446 self.enrich_booking_response(updated).await
448 }
449
450 pub async fn delete_booking(
456 &self,
457 booking_id: Uuid,
458 user_id: Uuid,
459 organization_id: Uuid,
460 ) -> Result<(), String> {
461 let owner = self.resolve_owner(user_id, organization_id).await?;
462 let booking = self
463 .booking_repo
464 .find_by_id(booking_id)
465 .await?
466 .ok_or("Booking not found".to_string())?;
467
468 if booking.booked_by != Some(owner.id) {
470 return Err("Only the booking owner can delete this booking".to_string());
471 }
472
473 self.booking_repo.delete(booking_id).await
474 }
475
476 pub async fn check_conflicts(
482 &self,
483 building_id: Uuid,
484 resource_type: ResourceType,
485 resource_name: String,
486 start_time: chrono::DateTime<Utc>,
487 end_time: chrono::DateTime<Utc>,
488 exclude_booking_id: Option<Uuid>,
489 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
490 let conflicts = self
491 .booking_repo
492 .find_conflicts(
493 building_id,
494 resource_type,
495 &resource_name,
496 start_time,
497 end_time,
498 exclude_booking_id,
499 )
500 .await?;
501
502 self.enrich_bookings_response(conflicts).await
503 }
504
505 pub async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
507 self.booking_repo.get_statistics(building_id).await
508 }
509
510 async fn enrich_booking_response(
517 &self,
518 booking: ResourceBooking,
519 ) -> Result<ResourceBookingResponseDto, String> {
520 let booked_by_name = if booking.on_behalf_of_acp {
521 "Syndic — pour le compte de l'ACP".to_string()
522 } else {
523 let owner_id = booking
524 .booked_by
525 .ok_or("Booking has neither an owner nor on_behalf_of_acp".to_string())?;
526 let owner = self
527 .owner_repo
528 .find_by_id(owner_id)
529 .await?
530 .ok_or("Booking owner not found".to_string())?;
531 format!("{} {}", owner.first_name, owner.last_name)
532 };
533
534 Ok(ResourceBookingResponseDto::from_entity(
535 booking,
536 booked_by_name,
537 ))
538 }
539
540 async fn enrich_bookings_response(
542 &self,
543 bookings: Vec<ResourceBooking>,
544 ) -> Result<Vec<ResourceBookingResponseDto>, String> {
545 let mut result = Vec::with_capacity(bookings.len());
546
547 for booking in bookings {
548 let enriched = self.enrich_booking_response(booking).await?;
549 result.push(enriched);
550 }
551
552 Ok(result)
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::application::dto::{BookingStatisticsDto, OwnerFilters, PageRequest};
560 use crate::application::ports::{OwnerRepository, ResourceBookingRepository};
561 use crate::domain::entities::{
562 BookingStatus, Owner, RecurringPattern, ResourceBooking, ResourceType,
563 };
564 use async_trait::async_trait;
565 use chrono::{DateTime, Duration, Utc};
566 use std::collections::HashMap;
567 use std::sync::{Arc, Mutex};
568 use uuid::Uuid;
569
570 struct MockBookingRepo {
572 bookings: Mutex<HashMap<Uuid, ResourceBooking>>,
573 }
574
575 impl MockBookingRepo {
576 fn new() -> Self {
577 Self {
578 bookings: Mutex::new(HashMap::new()),
579 }
580 }
581 }
582
583 #[async_trait]
584 impl ResourceBookingRepository for MockBookingRepo {
585 async fn create(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
586 let mut map = self.bookings.lock().unwrap();
587 map.insert(booking.id, booking.clone());
588 Ok(booking.clone())
589 }
590
591 async fn find_by_id(&self, id: Uuid) -> Result<Option<ResourceBooking>, String> {
592 let map = self.bookings.lock().unwrap();
593 Ok(map.get(&id).cloned())
594 }
595
596 async fn find_by_building(
597 &self,
598 building_id: Uuid,
599 ) -> Result<Vec<ResourceBooking>, String> {
600 let map = self.bookings.lock().unwrap();
601 Ok(map
602 .values()
603 .filter(|b| b.building_id == building_id)
604 .cloned()
605 .collect())
606 }
607
608 async fn find_by_building_and_resource_type(
609 &self,
610 building_id: Uuid,
611 resource_type: ResourceType,
612 ) -> Result<Vec<ResourceBooking>, String> {
613 let map = self.bookings.lock().unwrap();
614 Ok(map
615 .values()
616 .filter(|b| b.building_id == building_id && b.resource_type == resource_type)
617 .cloned()
618 .collect())
619 }
620
621 async fn find_by_resource(
622 &self,
623 building_id: Uuid,
624 resource_type: ResourceType,
625 resource_name: &str,
626 ) -> Result<Vec<ResourceBooking>, String> {
627 let map = self.bookings.lock().unwrap();
628 Ok(map
629 .values()
630 .filter(|b| {
631 b.building_id == building_id
632 && b.resource_type == resource_type
633 && b.resource_name == resource_name
634 })
635 .cloned()
636 .collect())
637 }
638
639 async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
640 let map = self.bookings.lock().unwrap();
641 Ok(map
642 .values()
643 .filter(|b| b.booked_by == Some(user_id))
644 .cloned()
645 .collect())
646 }
647
648 async fn find_by_user_and_status(
649 &self,
650 user_id: Uuid,
651 status: BookingStatus,
652 ) -> Result<Vec<ResourceBooking>, String> {
653 let map = self.bookings.lock().unwrap();
654 Ok(map
655 .values()
656 .filter(|b| b.booked_by == Some(user_id) && b.status == status)
657 .cloned()
658 .collect())
659 }
660
661 async fn find_by_building_and_status(
662 &self,
663 building_id: Uuid,
664 status: BookingStatus,
665 ) -> Result<Vec<ResourceBooking>, String> {
666 let map = self.bookings.lock().unwrap();
667 Ok(map
668 .values()
669 .filter(|b| b.building_id == building_id && b.status == status)
670 .cloned()
671 .collect())
672 }
673
674 async fn find_upcoming(
675 &self,
676 building_id: Uuid,
677 _limit: Option<i64>,
678 ) -> Result<Vec<ResourceBooking>, String> {
679 let map = self.bookings.lock().unwrap();
680 let now = Utc::now();
681 Ok(map
682 .values()
683 .filter(|b| {
684 b.building_id == building_id
685 && b.start_time > now
686 && matches!(b.status, BookingStatus::Pending | BookingStatus::Confirmed)
687 })
688 .cloned()
689 .collect())
690 }
691
692 async fn find_active(&self, building_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
693 let map = self.bookings.lock().unwrap();
694 let now = Utc::now();
695 Ok(map
696 .values()
697 .filter(|b| {
698 b.building_id == building_id
699 && b.status == BookingStatus::Confirmed
700 && now >= b.start_time
701 && now < b.end_time
702 })
703 .cloned()
704 .collect())
705 }
706
707 async fn find_past(
708 &self,
709 building_id: Uuid,
710 _limit: Option<i64>,
711 ) -> Result<Vec<ResourceBooking>, String> {
712 let map = self.bookings.lock().unwrap();
713 let now = Utc::now();
714 Ok(map
715 .values()
716 .filter(|b| b.building_id == building_id && b.end_time < now)
717 .cloned()
718 .collect())
719 }
720
721 async fn find_conflicts(
722 &self,
723 building_id: Uuid,
724 resource_type: ResourceType,
725 resource_name: &str,
726 start_time: DateTime<Utc>,
727 end_time: DateTime<Utc>,
728 exclude_booking_id: Option<Uuid>,
729 ) -> Result<Vec<ResourceBooking>, String> {
730 let map = self.bookings.lock().unwrap();
731 Ok(map
732 .values()
733 .filter(|b| {
734 b.building_id == building_id
735 && b.resource_type == resource_type
736 && b.resource_name == resource_name
737 && matches!(b.status, BookingStatus::Pending | BookingStatus::Confirmed)
738 && b.start_time < end_time
739 && start_time < b.end_time
740 && exclude_booking_id.is_none_or(|id| b.id != id)
741 })
742 .cloned()
743 .collect())
744 }
745
746 async fn update(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
747 let mut map = self.bookings.lock().unwrap();
748 map.insert(booking.id, booking.clone());
749 Ok(booking.clone())
750 }
751
752 async fn delete(&self, id: Uuid) -> Result<(), String> {
753 let mut map = self.bookings.lock().unwrap();
754 map.remove(&id);
755 Ok(())
756 }
757
758 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
759 let map = self.bookings.lock().unwrap();
760 Ok(map
761 .values()
762 .filter(|b| b.building_id == building_id)
763 .count() as i64)
764 }
765
766 async fn count_by_building_and_status(
767 &self,
768 building_id: Uuid,
769 status: BookingStatus,
770 ) -> Result<i64, String> {
771 let map = self.bookings.lock().unwrap();
772 Ok(map
773 .values()
774 .filter(|b| b.building_id == building_id && b.status == status)
775 .count() as i64)
776 }
777
778 async fn count_by_resource(
779 &self,
780 building_id: Uuid,
781 resource_type: ResourceType,
782 resource_name: &str,
783 ) -> Result<i64, String> {
784 let map = self.bookings.lock().unwrap();
785 Ok(map
786 .values()
787 .filter(|b| {
788 b.building_id == building_id
789 && b.resource_type == resource_type
790 && b.resource_name == resource_name
791 })
792 .count() as i64)
793 }
794
795 async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
796 Ok(BookingStatisticsDto {
797 building_id,
798 total_bookings: 0,
799 confirmed_bookings: 0,
800 pending_bookings: 0,
801 cancelled_bookings: 0,
802 completed_bookings: 0,
803 no_show_bookings: 0,
804 active_bookings: 0,
805 upcoming_bookings: 0,
806 total_hours_booked: 0.0,
807 most_popular_resource: None,
808 })
809 }
810 }
811
812 struct MockOwnerRepo {
814 owners: Mutex<HashMap<Uuid, Owner>>,
815 }
816
817 impl MockOwnerRepo {
818 fn new() -> Self {
819 Self {
820 owners: Mutex::new(HashMap::new()),
821 }
822 }
823
824 fn add_owner(&self, owner: Owner) {
825 let mut map = self.owners.lock().unwrap();
826 map.insert(owner.id, owner);
827 }
828 }
829
830 #[async_trait]
831 impl OwnerRepository for MockOwnerRepo {
832 async fn create(&self, owner: &Owner) -> Result<Owner, String> {
833 let mut map = self.owners.lock().unwrap();
834 map.insert(owner.id, owner.clone());
835 Ok(owner.clone())
836 }
837
838 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
839 let map = self.owners.lock().unwrap();
840 Ok(map.get(&id).cloned())
841 }
842
843 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
844 let map = self.owners.lock().unwrap();
845 Ok(map.values().find(|o| o.user_id == Some(user_id)).cloned())
846 }
847
848 async fn find_by_user_id_and_organization(
849 &self,
850 user_id: Uuid,
851 organization_id: Uuid,
852 ) -> Result<Option<Owner>, String> {
853 let map = self.owners.lock().unwrap();
854 Ok(map
855 .values()
856 .find(|o| o.user_id == Some(user_id) && o.organization_id == organization_id)
857 .cloned())
858 }
859
860 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
861 let map = self.owners.lock().unwrap();
862 Ok(map.values().find(|o| o.email == email).cloned())
863 }
864
865 async fn find_all(&self) -> Result<Vec<Owner>, String> {
866 let map = self.owners.lock().unwrap();
867 Ok(map.values().cloned().collect())
868 }
869
870 async fn find_all_paginated(
871 &self,
872 _page_request: &PageRequest,
873 _filters: &OwnerFilters,
874 ) -> Result<(Vec<Owner>, i64), String> {
875 let map = self.owners.lock().unwrap();
876 let all: Vec<_> = map.values().cloned().collect();
877 let count = all.len() as i64;
878 Ok((all, count))
879 }
880
881 async fn update(&self, owner: &Owner) -> Result<Owner, String> {
882 let mut map = self.owners.lock().unwrap();
883 map.insert(owner.id, owner.clone());
884 Ok(owner.clone())
885 }
886
887 async fn delete(&self, id: Uuid) -> Result<bool, String> {
888 let mut map = self.owners.lock().unwrap();
889 Ok(map.remove(&id).is_some())
890 }
891
892 async fn set_user_link(
893 &self,
894 owner_id: Uuid,
895 user_id: Option<Uuid>,
896 ) -> Result<bool, String> {
897 let mut map = self.owners.lock().unwrap();
898 if let Some(o) = map.get_mut(&owner_id) {
899 o.user_id = user_id;
900 Ok(true)
901 } else {
902 Ok(false)
903 }
904 }
905 }
906
907 fn create_test_owner(user_id: Uuid, organization_id: Uuid) -> Owner {
909 let mut owner = Owner::new(
910 organization_id,
911 "Jean".to_string(),
912 "Dupont".to_string(),
913 "jean@test.com".to_string(),
914 None,
915 "Rue Test 1".to_string(),
916 "Brussels".to_string(),
917 "1000".to_string(),
918 "Belgium".to_string(),
919 )
920 .unwrap();
921 owner.user_id = Some(user_id);
922 owner
923 }
924
925 fn setup_use_cases() -> (
926 ResourceBookingUseCases,
927 Uuid,
928 Uuid,
929 Uuid,
930 Arc<MockBookingRepo>,
931 ) {
932 let user_id = Uuid::new_v4();
933 let organization_id = Uuid::new_v4();
934 let building_id = Uuid::new_v4();
935
936 let booking_repo = Arc::new(MockBookingRepo::new());
937 let owner_repo = Arc::new(MockOwnerRepo::new());
938
939 let owner = create_test_owner(user_id, organization_id);
940 owner_repo.add_owner(owner);
941
942 let use_cases = ResourceBookingUseCases::new(
943 booking_repo.clone() as Arc<dyn ResourceBookingRepository>,
944 owner_repo as Arc<dyn OwnerRepository>,
945 );
946
947 (
948 use_cases,
949 user_id,
950 organization_id,
951 building_id,
952 booking_repo,
953 )
954 }
955
956 fn make_create_dto(building_id: Uuid) -> CreateResourceBookingDto {
957 let start_time = Utc::now() + Duration::hours(2);
958 let end_time = start_time + Duration::hours(2);
959 CreateResourceBookingDto {
960 building_id,
961 resource_type: ResourceType::MeetingRoom,
962 resource_name: "Meeting Room A".to_string(),
963 start_time,
964 end_time,
965 notes: Some("Team standup".to_string()),
966 recurring_pattern: RecurringPattern::None,
967 recurrence_end_date: None,
968 max_duration_hours: None,
969 max_advance_days: None,
970 on_behalf_of_acp: false,
971 motif: None,
972 }
973 }
974
975 #[tokio::test]
978 async fn test_create_booking_success() {
979 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
980 let dto = make_create_dto(building_id);
981
982 let result = use_cases.create_booking(user_id, org_id, false, dto).await;
983 assert!(result.is_ok());
984 let response = result.unwrap();
985 assert_eq!(response.building_id, building_id);
986 assert_eq!(response.resource_name, "Meeting Room A");
987 assert_eq!(response.booked_by_name, "Jean Dupont");
988 }
989
990 #[tokio::test]
991 async fn test_create_booking_conflict_detected() {
992 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
993 let dto = make_create_dto(building_id);
994
995 use_cases
997 .create_booking(user_id, org_id, false, dto.clone())
998 .await
999 .unwrap();
1000
1001 let result = use_cases.create_booking(user_id, org_id, false, dto).await;
1003 assert!(result.is_err());
1004 assert!(result.unwrap_err().contains("conflicts with"));
1005 }
1006
1007 #[tokio::test]
1008 async fn test_get_booking_success() {
1009 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1010 let dto = make_create_dto(building_id);
1011
1012 let created = use_cases
1013 .create_booking(user_id, org_id, false, dto)
1014 .await
1015 .unwrap();
1016
1017 let result = use_cases.get_booking(created.id).await;
1018 assert!(result.is_ok());
1019 assert_eq!(result.unwrap().id, created.id);
1020 }
1021
1022 #[tokio::test]
1023 async fn test_get_booking_not_found() {
1024 let (use_cases, _, _, _, _) = setup_use_cases();
1025 let result = use_cases.get_booking(Uuid::new_v4()).await;
1026 assert!(result.is_err());
1027 assert_eq!(result.unwrap_err(), "Booking not found");
1028 }
1029
1030 #[tokio::test]
1031 async fn test_cancel_booking_success() {
1032 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1033 let dto = make_create_dto(building_id);
1034
1035 let created = use_cases
1036 .create_booking(user_id, org_id, false, dto)
1037 .await
1038 .unwrap();
1039
1040 let result = use_cases.cancel_booking(created.id, user_id, org_id).await;
1041 assert!(result.is_ok());
1042 let cancelled = result.unwrap();
1043 assert_eq!(cancelled.status, BookingStatus::Cancelled);
1044 }
1045
1046 #[tokio::test]
1047 async fn test_cancel_booking_wrong_user() {
1048 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1049 let dto = make_create_dto(building_id);
1050
1051 let created = use_cases
1052 .create_booking(user_id, org_id, false, dto)
1053 .await
1054 .unwrap();
1055
1056 let other_user = Uuid::new_v4();
1058 let result = use_cases
1059 .cancel_booking(created.id, other_user, org_id)
1060 .await;
1061 assert!(result.is_err());
1062 assert_eq!(
1063 result.unwrap_err(),
1064 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES,
1065 "le refus doit être celui, nommé, opposé à qui n'est pas copropriétaire"
1066 );
1067 }
1068
1069 #[tokio::test]
1070 async fn test_delete_booking_success() {
1071 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1072 let dto = make_create_dto(building_id);
1073
1074 let created = use_cases
1075 .create_booking(user_id, org_id, false, dto)
1076 .await
1077 .unwrap();
1078
1079 let result = use_cases.delete_booking(created.id, user_id, org_id).await;
1080 assert!(result.is_ok());
1081
1082 let fetch = use_cases.get_booking(created.id).await;
1084 assert!(fetch.is_err());
1085 }
1086
1087 #[tokio::test]
1088 async fn test_confirm_booking_success() {
1089 let (use_cases, user_id, org_id, building_id, _booking_repo) = setup_use_cases();
1090 let dto = make_create_dto(building_id);
1091
1092 let created = use_cases
1093 .create_booking(user_id, org_id, false, dto)
1094 .await
1095 .unwrap();
1096
1097 let result = use_cases.confirm_booking(created.id).await;
1099 assert!(result.is_ok());
1100 let confirmed = result.unwrap();
1101 assert_eq!(confirmed.status, BookingStatus::Confirmed);
1102
1103 let completed = use_cases.complete_booking(created.id).await;
1105 assert!(completed.is_ok());
1106 assert_eq!(completed.unwrap().status, BookingStatus::Completed);
1107 }
1108
1109 #[tokio::test]
1110 async fn test_list_building_bookings() {
1111 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1112
1113 let dto1 = make_create_dto(building_id);
1114
1115 let mut dto2 = make_create_dto(building_id);
1116 dto2.resource_name = "Meeting Room B".to_string();
1117
1118 use_cases
1119 .create_booking(user_id, org_id, false, dto1)
1120 .await
1121 .unwrap();
1122 use_cases
1123 .create_booking(user_id, org_id, false, dto2)
1124 .await
1125 .unwrap();
1126
1127 let result = use_cases.list_building_bookings(building_id).await;
1128 assert!(result.is_ok());
1129 assert_eq!(result.unwrap().len(), 2);
1130 }
1131
1132 #[tokio::test]
1133 async fn test_owner_not_found_for_user() {
1134 let booking_repo = Arc::new(MockBookingRepo::new());
1135 let owner_repo = Arc::new(MockOwnerRepo::new());
1136 let use_cases = ResourceBookingUseCases::new(
1138 booking_repo as Arc<dyn ResourceBookingRepository>,
1139 owner_repo as Arc<dyn OwnerRepository>,
1140 );
1141
1142 let building_id = Uuid::new_v4();
1143 let dto = make_create_dto(building_id);
1144 let result = use_cases
1145 .create_booking(Uuid::new_v4(), Uuid::new_v4(), false, dto)
1146 .await;
1147 assert!(result.is_err());
1148 assert_eq!(
1149 result.unwrap_err(),
1150 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES,
1151 "le refus doit être celui, nommé, opposé à qui n'est pas copropriétaire"
1152 );
1153 }
1154
1155 fn make_on_behalf_dto(building_id: Uuid, motif: Option<&str>) -> CreateResourceBookingDto {
1160 let mut dto = make_create_dto(building_id);
1161 dto.on_behalf_of_acp = true;
1162 dto.motif = motif.map(|m| m.to_string());
1163 dto
1164 }
1165
1166 #[tokio::test]
1167 async fn happy_syndic_books_on_behalf_of_acp_with_motif() {
1168 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1169 let dto = make_on_behalf_dto(building_id, Some("AG annuelle"));
1170
1171 let result = use_cases.create_booking(user_id, org_id, true, dto).await;
1176 assert!(result.is_ok(), "{:?}", result.err());
1177 let response = result.unwrap();
1178 assert!(response.on_behalf_of_acp);
1179 assert_eq!(response.motif.as_deref(), Some("AG annuelle"));
1180 assert_eq!(response.booked_by, None);
1181 assert_eq!(response.booked_by_name, "Syndic — pour le compte de l'ACP");
1182 }
1183
1184 #[tokio::test]
1185 async fn negative_on_behalf_of_acp_without_motif_is_refused() {
1186 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1187 let dto = make_on_behalf_dto(building_id, None);
1188
1189 let result = use_cases.create_booking(user_id, org_id, true, dto).await;
1190 assert_eq!(
1191 result.unwrap_err(),
1192 crate::domain::entities::ReservationOnBehalfError::MotifRequired.to_string()
1193 );
1194 }
1195
1196 #[tokio::test]
1197 async fn security_non_syndic_cannot_set_on_behalf_of_acp() {
1198 let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1199 let dto = make_on_behalf_dto(building_id, Some("AG annuelle"));
1200
1201 let result = use_cases.create_booking(user_id, org_id, false, dto).await;
1205 assert_eq!(
1206 result.unwrap_err(),
1207 crate::application::error::REFUS_ON_BEHALF_RESERVE_AUX_SYNDICS,
1208 );
1209 }
1210
1211 #[tokio::test]
1212 async fn edge_syndic_without_owner_profile_and_on_behalf_of_acp_false_is_refused() {
1213 let booking_repo = Arc::new(MockBookingRepo::new());
1218 let owner_repo = Arc::new(MockOwnerRepo::new()); let use_cases = ResourceBookingUseCases::new(
1220 booking_repo as Arc<dyn ResourceBookingRepository>,
1221 owner_repo as Arc<dyn OwnerRepository>,
1222 );
1223 let building_id = Uuid::new_v4();
1224 let dto = make_create_dto(building_id); let result = use_cases
1227 .create_booking(Uuid::new_v4(), Uuid::new_v4(), true, dto)
1228 .await;
1229 assert_eq!(
1230 result.unwrap_err(),
1231 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES,
1232 );
1233 }
1234}