koprogo_api/application/use_cases/
resource_booking_use_cases.rs

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
11/// Use cases for resource booking operations
12///
13/// Orchestrates business logic for community space bookings with conflict detection,
14/// owner name enrichment, and authorization checks.
15pub 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    /// Resolve user_id to owner via organization lookup
32    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(|| "Owner not found for this user in the organization".to_string())
41    }
42
43    /// Create a new resource booking with conflict detection
44    ///
45    /// # Steps
46    /// 1. Create booking entity (validates business rules)
47    /// 2. Check for conflicts (overlapping bookings for same resource)
48    /// 3. Persist booking
49    /// 4. Enrich response with owner name
50    ///
51    /// # Authorization
52    /// - Any authenticated owner can create bookings
53    pub async fn create_booking(
54        &self,
55        user_id: Uuid,
56        organization_id: Uuid,
57        dto: CreateResourceBookingDto,
58    ) -> Result<ResourceBookingResponseDto, String> {
59        let owner = self.resolve_owner(user_id, organization_id).await?;
60        let booked_by = owner.id;
61        // Create booking entity (validates business rules in constructor)
62        let booking = ResourceBooking::new(
63            dto.building_id,
64            dto.resource_type.clone(),
65            dto.resource_name.clone(),
66            booked_by,
67            dto.start_time,
68            dto.end_time,
69            dto.notes.clone(),
70            dto.recurring_pattern.clone(),
71            dto.recurrence_end_date,
72            dto.max_duration_hours,
73            dto.max_advance_days,
74        )?;
75
76        // Check for conflicts (overlapping bookings for same resource)
77        let conflicts = self
78            .booking_repo
79            .find_conflicts(
80                dto.building_id,
81                dto.resource_type,
82                &dto.resource_name,
83                dto.start_time,
84                dto.end_time,
85                None, // No exclusion for new bookings
86            )
87            .await?;
88
89        if !conflicts.is_empty() {
90            return Err(format!(
91                "Booking conflicts with {} existing booking(s) for this resource",
92                conflicts.len()
93            ));
94        }
95
96        // Persist booking
97        let created = self.booking_repo.create(&booking).await?;
98
99        // Enrich with owner name
100        self.enrich_booking_response(created).await
101    }
102
103    /// Get booking by ID with owner name enrichment
104    pub async fn get_booking(
105        &self,
106        booking_id: Uuid,
107    ) -> Result<ResourceBookingResponseDto, String> {
108        let booking = self
109            .booking_repo
110            .find_by_id(booking_id)
111            .await?
112            .ok_or("Booking not found".to_string())?;
113
114        self.enrich_booking_response(booking).await
115    }
116
117    /// List all bookings for a building
118    pub async fn list_building_bookings(
119        &self,
120        building_id: Uuid,
121    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
122        let bookings = self.booking_repo.find_by_building(building_id).await?;
123        self.enrich_bookings_response(bookings).await
124    }
125
126    /// List bookings by building and resource type
127    pub async fn list_by_resource_type(
128        &self,
129        building_id: Uuid,
130        resource_type: ResourceType,
131    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
132        let bookings = self
133            .booking_repo
134            .find_by_building_and_resource_type(building_id, resource_type)
135            .await?;
136        self.enrich_bookings_response(bookings).await
137    }
138
139    /// List bookings for a specific resource (building + type + name)
140    pub async fn list_by_resource(
141        &self,
142        building_id: Uuid,
143        resource_type: ResourceType,
144        resource_name: String,
145    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
146        let bookings = self
147            .booking_repo
148            .find_by_resource(building_id, resource_type, &resource_name)
149            .await?;
150        self.enrich_bookings_response(bookings).await
151    }
152
153    /// List bookings by user (owner who made the booking)
154    pub async fn list_user_bookings(
155        &self,
156        user_id: Uuid,
157        organization_id: Uuid,
158    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
159        let owner = self.resolve_owner(user_id, organization_id).await?;
160        let bookings = self.booking_repo.find_by_user(owner.id).await?;
161        self.enrich_bookings_response(bookings).await
162    }
163
164    /// List bookings by user and status
165    pub async fn list_user_bookings_by_status(
166        &self,
167        user_id: Uuid,
168        organization_id: Uuid,
169        status: BookingStatus,
170    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
171        let owner = self.resolve_owner(user_id, organization_id).await?;
172        let bookings = self
173            .booking_repo
174            .find_by_user_and_status(owner.id, status)
175            .await?;
176        self.enrich_bookings_response(bookings).await
177    }
178
179    /// List bookings by building and status
180    pub async fn list_building_bookings_by_status(
181        &self,
182        building_id: Uuid,
183        status: BookingStatus,
184    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
185        let bookings = self
186            .booking_repo
187            .find_by_building_and_status(building_id, status)
188            .await?;
189        self.enrich_bookings_response(bookings).await
190    }
191
192    /// List upcoming bookings (future, confirmed or pending)
193    pub async fn list_upcoming_bookings(
194        &self,
195        building_id: Uuid,
196        limit: Option<i64>,
197    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
198        let bookings = self.booking_repo.find_upcoming(building_id, limit).await?;
199        self.enrich_bookings_response(bookings).await
200    }
201
202    /// List active bookings (currently in progress)
203    pub async fn list_active_bookings(
204        &self,
205        building_id: Uuid,
206    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
207        let bookings = self.booking_repo.find_active(building_id).await?;
208        self.enrich_bookings_response(bookings).await
209    }
210
211    /// List past bookings
212    pub async fn list_past_bookings(
213        &self,
214        building_id: Uuid,
215        limit: Option<i64>,
216    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
217        let bookings = self.booking_repo.find_past(building_id, limit).await?;
218        self.enrich_bookings_response(bookings).await
219    }
220
221    /// Update booking details (resource_name, notes)
222    ///
223    /// Time changes require cancellation and rebooking to ensure conflict detection.
224    ///
225    /// # Authorization
226    /// - Only booking owner can update their booking
227    pub async fn update_booking(
228        &self,
229        booking_id: Uuid,
230        user_id: Uuid,
231        organization_id: Uuid,
232        dto: UpdateResourceBookingDto,
233    ) -> Result<ResourceBookingResponseDto, String> {
234        let owner = self.resolve_owner(user_id, organization_id).await?;
235        let mut booking = self
236            .booking_repo
237            .find_by_id(booking_id)
238            .await?
239            .ok_or("Booking not found".to_string())?;
240
241        // Authorization: Only booking owner can update
242        if booking.booked_by != owner.id {
243            return Err("Only the booking owner can update this booking".to_string());
244        }
245
246        // Update details (validates business rules)
247        booking.update_details(dto.resource_name, dto.notes)?;
248
249        // Persist changes
250        let updated = self.booking_repo.update(&booking).await?;
251
252        // Enrich with owner name
253        self.enrich_booking_response(updated).await
254    }
255
256    /// Cancel a booking
257    ///
258    /// # Authorization
259    /// - Only booking owner can cancel their booking
260    pub async fn cancel_booking(
261        &self,
262        booking_id: Uuid,
263        user_id: Uuid,
264        organization_id: Uuid,
265    ) -> Result<ResourceBookingResponseDto, String> {
266        let owner = self.resolve_owner(user_id, organization_id).await?;
267        let mut booking = self
268            .booking_repo
269            .find_by_id(booking_id)
270            .await?
271            .ok_or("Booking not found".to_string())?;
272
273        // Cancel booking (validates authorization and state)
274        booking.cancel(owner.id)?;
275
276        // Persist changes
277        let updated = self.booking_repo.update(&booking).await?;
278
279        // Enrich with owner name
280        self.enrich_booking_response(updated).await
281    }
282
283    /// Complete a booking (typically auto-called after end_time)
284    ///
285    /// # Authorization
286    /// - Admin only (for manual completion)
287    pub async fn complete_booking(
288        &self,
289        booking_id: Uuid,
290    ) -> Result<ResourceBookingResponseDto, String> {
291        let mut booking = self
292            .booking_repo
293            .find_by_id(booking_id)
294            .await?
295            .ok_or("Booking not found".to_string())?;
296
297        // Complete booking (validates state)
298        booking.complete()?;
299
300        // Persist changes
301        let updated = self.booking_repo.update(&booking).await?;
302
303        // Enrich with owner name
304        self.enrich_booking_response(updated).await
305    }
306
307    /// Mark booking as no-show
308    ///
309    /// # Authorization
310    /// - Admin only
311    pub async fn mark_no_show(
312        &self,
313        booking_id: Uuid,
314    ) -> Result<ResourceBookingResponseDto, String> {
315        let mut booking = self
316            .booking_repo
317            .find_by_id(booking_id)
318            .await?
319            .ok_or("Booking not found".to_string())?;
320
321        // Mark as no-show (validates state)
322        booking.mark_no_show()?;
323
324        // Persist changes
325        let updated = self.booking_repo.update(&booking).await?;
326
327        // Enrich with owner name
328        self.enrich_booking_response(updated).await
329    }
330
331    /// Confirm a pending booking
332    ///
333    /// # Authorization
334    /// - Admin only (for approval workflow)
335    pub async fn confirm_booking(
336        &self,
337        booking_id: Uuid,
338    ) -> Result<ResourceBookingResponseDto, String> {
339        let mut booking = self
340            .booking_repo
341            .find_by_id(booking_id)
342            .await?
343            .ok_or("Booking not found".to_string())?;
344
345        // Confirm booking (validates state)
346        booking.confirm()?;
347
348        // Persist changes
349        let updated = self.booking_repo.update(&booking).await?;
350
351        // Enrich with owner name
352        self.enrich_booking_response(updated).await
353    }
354
355    /// Delete a booking
356    ///
357    /// # Authorization
358    /// - Only booking owner can delete their booking
359    /// - Or admin for any booking
360    pub async fn delete_booking(
361        &self,
362        booking_id: Uuid,
363        user_id: Uuid,
364        organization_id: Uuid,
365    ) -> Result<(), String> {
366        let owner = self.resolve_owner(user_id, organization_id).await?;
367        let booking = self
368            .booking_repo
369            .find_by_id(booking_id)
370            .await?
371            .ok_or("Booking not found".to_string())?;
372
373        // Authorization: Only booking owner can delete
374        if booking.booked_by != owner.id {
375            return Err("Only the booking owner can delete this booking".to_string());
376        }
377
378        self.booking_repo.delete(booking_id).await
379    }
380
381    /// Check for booking conflicts
382    ///
383    /// Useful for frontend to preview conflicts before creating booking.
384    ///
385    /// Returns list of conflicting bookings with owner names.
386    pub async fn check_conflicts(
387        &self,
388        building_id: Uuid,
389        resource_type: ResourceType,
390        resource_name: String,
391        start_time: chrono::DateTime<Utc>,
392        end_time: chrono::DateTime<Utc>,
393        exclude_booking_id: Option<Uuid>,
394    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
395        let conflicts = self
396            .booking_repo
397            .find_conflicts(
398                building_id,
399                resource_type,
400                &resource_name,
401                start_time,
402                end_time,
403                exclude_booking_id,
404            )
405            .await?;
406
407        self.enrich_bookings_response(conflicts).await
408    }
409
410    /// Get booking statistics for a building
411    pub async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
412        self.booking_repo.get_statistics(building_id).await
413    }
414
415    /// Helper: Enrich single booking with owner name
416    async fn enrich_booking_response(
417        &self,
418        booking: ResourceBooking,
419    ) -> Result<ResourceBookingResponseDto, String> {
420        // Fetch owner to get full name
421        let owner = self
422            .owner_repo
423            .find_by_id(booking.booked_by)
424            .await?
425            .ok_or("Booking owner not found".to_string())?;
426
427        let booked_by_name = format!("{} {}", owner.first_name, owner.last_name);
428
429        Ok(ResourceBookingResponseDto::from_entity(
430            booking,
431            booked_by_name,
432        ))
433    }
434
435    /// Helper: Enrich multiple bookings with owner names
436    async fn enrich_bookings_response(
437        &self,
438        bookings: Vec<ResourceBooking>,
439    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
440        let mut result = Vec::with_capacity(bookings.len());
441
442        for booking in bookings {
443            let enriched = self.enrich_booking_response(booking).await?;
444            result.push(enriched);
445        }
446
447        Ok(result)
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use crate::application::dto::{BookingStatisticsDto, OwnerFilters, PageRequest};
455    use crate::application::ports::{OwnerRepository, ResourceBookingRepository};
456    use crate::domain::entities::{
457        BookingStatus, Owner, RecurringPattern, ResourceBooking, ResourceType,
458    };
459    use async_trait::async_trait;
460    use chrono::{DateTime, Duration, Utc};
461    use std::collections::HashMap;
462    use std::sync::{Arc, Mutex};
463    use uuid::Uuid;
464
465    // ── Mock ResourceBookingRepository ──────────────────────────────────────
466    struct MockBookingRepo {
467        bookings: Mutex<HashMap<Uuid, ResourceBooking>>,
468    }
469
470    impl MockBookingRepo {
471        fn new() -> Self {
472            Self {
473                bookings: Mutex::new(HashMap::new()),
474            }
475        }
476    }
477
478    #[async_trait]
479    impl ResourceBookingRepository for MockBookingRepo {
480        async fn create(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
481            let mut map = self.bookings.lock().unwrap();
482            map.insert(booking.id, booking.clone());
483            Ok(booking.clone())
484        }
485
486        async fn find_by_id(&self, id: Uuid) -> Result<Option<ResourceBooking>, String> {
487            let map = self.bookings.lock().unwrap();
488            Ok(map.get(&id).cloned())
489        }
490
491        async fn find_by_building(
492            &self,
493            building_id: Uuid,
494        ) -> Result<Vec<ResourceBooking>, String> {
495            let map = self.bookings.lock().unwrap();
496            Ok(map
497                .values()
498                .filter(|b| b.building_id == building_id)
499                .cloned()
500                .collect())
501        }
502
503        async fn find_by_building_and_resource_type(
504            &self,
505            building_id: Uuid,
506            resource_type: ResourceType,
507        ) -> Result<Vec<ResourceBooking>, String> {
508            let map = self.bookings.lock().unwrap();
509            Ok(map
510                .values()
511                .filter(|b| b.building_id == building_id && b.resource_type == resource_type)
512                .cloned()
513                .collect())
514        }
515
516        async fn find_by_resource(
517            &self,
518            building_id: Uuid,
519            resource_type: ResourceType,
520            resource_name: &str,
521        ) -> Result<Vec<ResourceBooking>, String> {
522            let map = self.bookings.lock().unwrap();
523            Ok(map
524                .values()
525                .filter(|b| {
526                    b.building_id == building_id
527                        && b.resource_type == resource_type
528                        && b.resource_name == resource_name
529                })
530                .cloned()
531                .collect())
532        }
533
534        async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
535            let map = self.bookings.lock().unwrap();
536            Ok(map
537                .values()
538                .filter(|b| b.booked_by == user_id)
539                .cloned()
540                .collect())
541        }
542
543        async fn find_by_user_and_status(
544            &self,
545            user_id: Uuid,
546            status: BookingStatus,
547        ) -> Result<Vec<ResourceBooking>, String> {
548            let map = self.bookings.lock().unwrap();
549            Ok(map
550                .values()
551                .filter(|b| b.booked_by == user_id && b.status == status)
552                .cloned()
553                .collect())
554        }
555
556        async fn find_by_building_and_status(
557            &self,
558            building_id: Uuid,
559            status: BookingStatus,
560        ) -> Result<Vec<ResourceBooking>, String> {
561            let map = self.bookings.lock().unwrap();
562            Ok(map
563                .values()
564                .filter(|b| b.building_id == building_id && b.status == status)
565                .cloned()
566                .collect())
567        }
568
569        async fn find_upcoming(
570            &self,
571            building_id: Uuid,
572            _limit: Option<i64>,
573        ) -> Result<Vec<ResourceBooking>, String> {
574            let map = self.bookings.lock().unwrap();
575            let now = Utc::now();
576            Ok(map
577                .values()
578                .filter(|b| {
579                    b.building_id == building_id
580                        && b.start_time > now
581                        && matches!(b.status, BookingStatus::Pending | BookingStatus::Confirmed)
582                })
583                .cloned()
584                .collect())
585        }
586
587        async fn find_active(&self, building_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
588            let map = self.bookings.lock().unwrap();
589            let now = Utc::now();
590            Ok(map
591                .values()
592                .filter(|b| {
593                    b.building_id == building_id
594                        && b.status == BookingStatus::Confirmed
595                        && now >= b.start_time
596                        && now < b.end_time
597                })
598                .cloned()
599                .collect())
600        }
601
602        async fn find_past(
603            &self,
604            building_id: Uuid,
605            _limit: Option<i64>,
606        ) -> Result<Vec<ResourceBooking>, String> {
607            let map = self.bookings.lock().unwrap();
608            let now = Utc::now();
609            Ok(map
610                .values()
611                .filter(|b| b.building_id == building_id && b.end_time < now)
612                .cloned()
613                .collect())
614        }
615
616        async fn find_conflicts(
617            &self,
618            building_id: Uuid,
619            resource_type: ResourceType,
620            resource_name: &str,
621            start_time: DateTime<Utc>,
622            end_time: DateTime<Utc>,
623            exclude_booking_id: Option<Uuid>,
624        ) -> Result<Vec<ResourceBooking>, String> {
625            let map = self.bookings.lock().unwrap();
626            Ok(map
627                .values()
628                .filter(|b| {
629                    b.building_id == building_id
630                        && b.resource_type == resource_type
631                        && b.resource_name == resource_name
632                        && matches!(b.status, BookingStatus::Pending | BookingStatus::Confirmed)
633                        && b.start_time < end_time
634                        && start_time < b.end_time
635                        && exclude_booking_id.is_none_or(|id| b.id != id)
636                })
637                .cloned()
638                .collect())
639        }
640
641        async fn update(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
642            let mut map = self.bookings.lock().unwrap();
643            map.insert(booking.id, booking.clone());
644            Ok(booking.clone())
645        }
646
647        async fn delete(&self, id: Uuid) -> Result<(), String> {
648            let mut map = self.bookings.lock().unwrap();
649            map.remove(&id);
650            Ok(())
651        }
652
653        async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
654            let map = self.bookings.lock().unwrap();
655            Ok(map
656                .values()
657                .filter(|b| b.building_id == building_id)
658                .count() as i64)
659        }
660
661        async fn count_by_building_and_status(
662            &self,
663            building_id: Uuid,
664            status: BookingStatus,
665        ) -> Result<i64, 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                .count() as i64)
671        }
672
673        async fn count_by_resource(
674            &self,
675            building_id: Uuid,
676            resource_type: ResourceType,
677            resource_name: &str,
678        ) -> Result<i64, String> {
679            let map = self.bookings.lock().unwrap();
680            Ok(map
681                .values()
682                .filter(|b| {
683                    b.building_id == building_id
684                        && b.resource_type == resource_type
685                        && b.resource_name == resource_name
686                })
687                .count() as i64)
688        }
689
690        async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
691            Ok(BookingStatisticsDto {
692                building_id,
693                total_bookings: 0,
694                confirmed_bookings: 0,
695                pending_bookings: 0,
696                cancelled_bookings: 0,
697                completed_bookings: 0,
698                no_show_bookings: 0,
699                active_bookings: 0,
700                upcoming_bookings: 0,
701                total_hours_booked: 0.0,
702                most_popular_resource: None,
703            })
704        }
705    }
706
707    // ── Mock OwnerRepository ────────────────────────────────────────────────
708    struct MockOwnerRepo {
709        owners: Mutex<HashMap<Uuid, Owner>>,
710    }
711
712    impl MockOwnerRepo {
713        fn new() -> Self {
714            Self {
715                owners: Mutex::new(HashMap::new()),
716            }
717        }
718
719        fn add_owner(&self, owner: Owner) {
720            let mut map = self.owners.lock().unwrap();
721            map.insert(owner.id, owner);
722        }
723    }
724
725    #[async_trait]
726    impl OwnerRepository for MockOwnerRepo {
727        async fn create(&self, owner: &Owner) -> Result<Owner, String> {
728            let mut map = self.owners.lock().unwrap();
729            map.insert(owner.id, owner.clone());
730            Ok(owner.clone())
731        }
732
733        async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
734            let map = self.owners.lock().unwrap();
735            Ok(map.get(&id).cloned())
736        }
737
738        async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
739            let map = self.owners.lock().unwrap();
740            Ok(map.values().find(|o| o.user_id == Some(user_id)).cloned())
741        }
742
743        async fn find_by_user_id_and_organization(
744            &self,
745            user_id: Uuid,
746            organization_id: Uuid,
747        ) -> Result<Option<Owner>, String> {
748            let map = self.owners.lock().unwrap();
749            Ok(map
750                .values()
751                .find(|o| o.user_id == Some(user_id) && o.organization_id == organization_id)
752                .cloned())
753        }
754
755        async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
756            let map = self.owners.lock().unwrap();
757            Ok(map.values().find(|o| o.email == email).cloned())
758        }
759
760        async fn find_all(&self) -> Result<Vec<Owner>, String> {
761            let map = self.owners.lock().unwrap();
762            Ok(map.values().cloned().collect())
763        }
764
765        async fn find_all_paginated(
766            &self,
767            _page_request: &PageRequest,
768            _filters: &OwnerFilters,
769        ) -> Result<(Vec<Owner>, i64), String> {
770            let map = self.owners.lock().unwrap();
771            let all: Vec<_> = map.values().cloned().collect();
772            let count = all.len() as i64;
773            Ok((all, count))
774        }
775
776        async fn update(&self, owner: &Owner) -> Result<Owner, String> {
777            let mut map = self.owners.lock().unwrap();
778            map.insert(owner.id, owner.clone());
779            Ok(owner.clone())
780        }
781
782        async fn delete(&self, id: Uuid) -> Result<bool, String> {
783            let mut map = self.owners.lock().unwrap();
784            Ok(map.remove(&id).is_some())
785        }
786
787        async fn set_user_link(
788            &self,
789            owner_id: Uuid,
790            user_id: Option<Uuid>,
791        ) -> Result<bool, String> {
792            let mut map = self.owners.lock().unwrap();
793            if let Some(o) = map.get_mut(&owner_id) {
794                o.user_id = user_id;
795                Ok(true)
796            } else {
797                Ok(false)
798            }
799        }
800    }
801
802    // ── Helpers ─────────────────────────────────────────────────────────────
803    fn create_test_owner(user_id: Uuid, organization_id: Uuid) -> Owner {
804        let mut owner = Owner::new(
805            organization_id,
806            "Jean".to_string(),
807            "Dupont".to_string(),
808            "jean@test.com".to_string(),
809            None,
810            "Rue Test 1".to_string(),
811            "Brussels".to_string(),
812            "1000".to_string(),
813            "Belgium".to_string(),
814        )
815        .unwrap();
816        owner.user_id = Some(user_id);
817        owner
818    }
819
820    fn setup_use_cases() -> (
821        ResourceBookingUseCases,
822        Uuid,
823        Uuid,
824        Uuid,
825        Arc<MockBookingRepo>,
826    ) {
827        let user_id = Uuid::new_v4();
828        let organization_id = Uuid::new_v4();
829        let building_id = Uuid::new_v4();
830
831        let booking_repo = Arc::new(MockBookingRepo::new());
832        let owner_repo = Arc::new(MockOwnerRepo::new());
833
834        let owner = create_test_owner(user_id, organization_id);
835        owner_repo.add_owner(owner);
836
837        let use_cases = ResourceBookingUseCases::new(
838            booking_repo.clone() as Arc<dyn ResourceBookingRepository>,
839            owner_repo as Arc<dyn OwnerRepository>,
840        );
841
842        (
843            use_cases,
844            user_id,
845            organization_id,
846            building_id,
847            booking_repo,
848        )
849    }
850
851    fn make_create_dto(building_id: Uuid) -> CreateResourceBookingDto {
852        let start_time = Utc::now() + Duration::hours(2);
853        let end_time = start_time + Duration::hours(2);
854        CreateResourceBookingDto {
855            building_id,
856            resource_type: ResourceType::MeetingRoom,
857            resource_name: "Meeting Room A".to_string(),
858            start_time,
859            end_time,
860            notes: Some("Team standup".to_string()),
861            recurring_pattern: RecurringPattern::None,
862            recurrence_end_date: None,
863            max_duration_hours: None,
864            max_advance_days: None,
865        }
866    }
867
868    // ── Tests ───────────────────────────────────────────────────────────────
869
870    #[tokio::test]
871    async fn test_create_booking_success() {
872        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
873        let dto = make_create_dto(building_id);
874
875        let result = use_cases.create_booking(user_id, org_id, dto).await;
876        assert!(result.is_ok());
877        let response = result.unwrap();
878        assert_eq!(response.building_id, building_id);
879        assert_eq!(response.resource_name, "Meeting Room A");
880        assert_eq!(response.booked_by_name, "Jean Dupont");
881    }
882
883    #[tokio::test]
884    async fn test_create_booking_conflict_detected() {
885        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
886        let dto = make_create_dto(building_id);
887
888        // First booking succeeds
889        use_cases
890            .create_booking(user_id, org_id, dto.clone())
891            .await
892            .unwrap();
893
894        // Second booking for same resource and time range should fail
895        let result = use_cases.create_booking(user_id, org_id, dto).await;
896        assert!(result.is_err());
897        assert!(result.unwrap_err().contains("conflicts with"));
898    }
899
900    #[tokio::test]
901    async fn test_get_booking_success() {
902        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
903        let dto = make_create_dto(building_id);
904
905        let created = use_cases
906            .create_booking(user_id, org_id, dto)
907            .await
908            .unwrap();
909
910        let result = use_cases.get_booking(created.id).await;
911        assert!(result.is_ok());
912        assert_eq!(result.unwrap().id, created.id);
913    }
914
915    #[tokio::test]
916    async fn test_get_booking_not_found() {
917        let (use_cases, _, _, _, _) = setup_use_cases();
918        let result = use_cases.get_booking(Uuid::new_v4()).await;
919        assert!(result.is_err());
920        assert_eq!(result.unwrap_err(), "Booking not found");
921    }
922
923    #[tokio::test]
924    async fn test_cancel_booking_success() {
925        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
926        let dto = make_create_dto(building_id);
927
928        let created = use_cases
929            .create_booking(user_id, org_id, dto)
930            .await
931            .unwrap();
932
933        let result = use_cases.cancel_booking(created.id, user_id, org_id).await;
934        assert!(result.is_ok());
935        let cancelled = result.unwrap();
936        assert_eq!(cancelled.status, BookingStatus::Cancelled);
937    }
938
939    #[tokio::test]
940    async fn test_cancel_booking_wrong_user() {
941        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
942        let dto = make_create_dto(building_id);
943
944        let created = use_cases
945            .create_booking(user_id, org_id, dto)
946            .await
947            .unwrap();
948
949        // Try cancelling with a different user who is not registered as owner
950        let other_user = Uuid::new_v4();
951        let result = use_cases
952            .cancel_booking(created.id, other_user, org_id)
953            .await;
954        assert!(result.is_err());
955        assert!(result.unwrap_err().contains("Owner not found"));
956    }
957
958    #[tokio::test]
959    async fn test_delete_booking_success() {
960        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
961        let dto = make_create_dto(building_id);
962
963        let created = use_cases
964            .create_booking(user_id, org_id, dto)
965            .await
966            .unwrap();
967
968        let result = use_cases.delete_booking(created.id, user_id, org_id).await;
969        assert!(result.is_ok());
970
971        // Confirm it's gone
972        let fetch = use_cases.get_booking(created.id).await;
973        assert!(fetch.is_err());
974    }
975
976    #[tokio::test]
977    async fn test_confirm_booking_success() {
978        let (use_cases, user_id, org_id, building_id, _booking_repo) = setup_use_cases();
979        let dto = make_create_dto(building_id);
980
981        let created = use_cases
982            .create_booking(user_id, org_id, dto)
983            .await
984            .unwrap();
985
986        // Manually confirm via the use case
987        let result = use_cases.confirm_booking(created.id).await;
988        assert!(result.is_ok());
989        let confirmed = result.unwrap();
990        assert_eq!(confirmed.status, BookingStatus::Confirmed);
991
992        // Now we can complete it
993        let completed = use_cases.complete_booking(created.id).await;
994        assert!(completed.is_ok());
995        assert_eq!(completed.unwrap().status, BookingStatus::Completed);
996    }
997
998    #[tokio::test]
999    async fn test_list_building_bookings() {
1000        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
1001
1002        let dto1 = make_create_dto(building_id);
1003
1004        let mut dto2 = make_create_dto(building_id);
1005        dto2.resource_name = "Meeting Room B".to_string();
1006
1007        use_cases
1008            .create_booking(user_id, org_id, dto1)
1009            .await
1010            .unwrap();
1011        use_cases
1012            .create_booking(user_id, org_id, dto2)
1013            .await
1014            .unwrap();
1015
1016        let result = use_cases.list_building_bookings(building_id).await;
1017        assert!(result.is_ok());
1018        assert_eq!(result.unwrap().len(), 2);
1019    }
1020
1021    #[tokio::test]
1022    async fn test_owner_not_found_for_user() {
1023        let booking_repo = Arc::new(MockBookingRepo::new());
1024        let owner_repo = Arc::new(MockOwnerRepo::new());
1025        // Do not add any owner
1026        let use_cases = ResourceBookingUseCases::new(
1027            booking_repo as Arc<dyn ResourceBookingRepository>,
1028            owner_repo as Arc<dyn OwnerRepository>,
1029        );
1030
1031        let building_id = Uuid::new_v4();
1032        let dto = make_create_dto(building_id);
1033        let result = use_cases
1034            .create_booking(Uuid::new_v4(), Uuid::new_v4(), dto)
1035            .await;
1036        assert!(result.is_err());
1037        assert!(result.unwrap_err().contains("Owner not found"));
1038    }
1039}