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.map_or(true, |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
788    // ── Helpers ─────────────────────────────────────────────────────────────
789    fn create_test_owner(user_id: Uuid, organization_id: Uuid) -> Owner {
790        let mut owner = Owner::new(
791            organization_id,
792            "Jean".to_string(),
793            "Dupont".to_string(),
794            "jean@test.com".to_string(),
795            None,
796            "Rue Test 1".to_string(),
797            "Brussels".to_string(),
798            "1000".to_string(),
799            "Belgium".to_string(),
800        )
801        .unwrap();
802        owner.user_id = Some(user_id);
803        owner
804    }
805
806    fn setup_use_cases() -> (
807        ResourceBookingUseCases,
808        Uuid,
809        Uuid,
810        Uuid,
811        Arc<MockBookingRepo>,
812    ) {
813        let user_id = Uuid::new_v4();
814        let organization_id = Uuid::new_v4();
815        let building_id = Uuid::new_v4();
816
817        let booking_repo = Arc::new(MockBookingRepo::new());
818        let owner_repo = Arc::new(MockOwnerRepo::new());
819
820        let owner = create_test_owner(user_id, organization_id);
821        owner_repo.add_owner(owner);
822
823        let use_cases = ResourceBookingUseCases::new(
824            booking_repo.clone() as Arc<dyn ResourceBookingRepository>,
825            owner_repo as Arc<dyn OwnerRepository>,
826        );
827
828        (
829            use_cases,
830            user_id,
831            organization_id,
832            building_id,
833            booking_repo,
834        )
835    }
836
837    fn make_create_dto(building_id: Uuid) -> CreateResourceBookingDto {
838        let start_time = Utc::now() + Duration::hours(2);
839        let end_time = start_time + Duration::hours(2);
840        CreateResourceBookingDto {
841            building_id,
842            resource_type: ResourceType::MeetingRoom,
843            resource_name: "Meeting Room A".to_string(),
844            start_time,
845            end_time,
846            notes: Some("Team standup".to_string()),
847            recurring_pattern: RecurringPattern::None,
848            recurrence_end_date: None,
849            max_duration_hours: None,
850            max_advance_days: None,
851        }
852    }
853
854    // ── Tests ───────────────────────────────────────────────────────────────
855
856    #[tokio::test]
857    async fn test_create_booking_success() {
858        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
859        let dto = make_create_dto(building_id);
860
861        let result = use_cases.create_booking(user_id, org_id, dto).await;
862        assert!(result.is_ok());
863        let response = result.unwrap();
864        assert_eq!(response.building_id, building_id);
865        assert_eq!(response.resource_name, "Meeting Room A");
866        assert_eq!(response.booked_by_name, "Jean Dupont");
867    }
868
869    #[tokio::test]
870    async fn test_create_booking_conflict_detected() {
871        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
872        let dto = make_create_dto(building_id);
873
874        // First booking succeeds
875        use_cases
876            .create_booking(user_id, org_id, dto.clone())
877            .await
878            .unwrap();
879
880        // Second booking for same resource and time range should fail
881        let result = use_cases.create_booking(user_id, org_id, dto).await;
882        assert!(result.is_err());
883        assert!(result.unwrap_err().contains("conflicts with"));
884    }
885
886    #[tokio::test]
887    async fn test_get_booking_success() {
888        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
889        let dto = make_create_dto(building_id);
890
891        let created = use_cases
892            .create_booking(user_id, org_id, dto)
893            .await
894            .unwrap();
895
896        let result = use_cases.get_booking(created.id).await;
897        assert!(result.is_ok());
898        assert_eq!(result.unwrap().id, created.id);
899    }
900
901    #[tokio::test]
902    async fn test_get_booking_not_found() {
903        let (use_cases, _, _, _, _) = setup_use_cases();
904        let result = use_cases.get_booking(Uuid::new_v4()).await;
905        assert!(result.is_err());
906        assert_eq!(result.unwrap_err(), "Booking not found");
907    }
908
909    #[tokio::test]
910    async fn test_cancel_booking_success() {
911        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
912        let dto = make_create_dto(building_id);
913
914        let created = use_cases
915            .create_booking(user_id, org_id, dto)
916            .await
917            .unwrap();
918
919        let result = use_cases.cancel_booking(created.id, user_id, org_id).await;
920        assert!(result.is_ok());
921        let cancelled = result.unwrap();
922        assert_eq!(cancelled.status, BookingStatus::Cancelled);
923    }
924
925    #[tokio::test]
926    async fn test_cancel_booking_wrong_user() {
927        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
928        let dto = make_create_dto(building_id);
929
930        let created = use_cases
931            .create_booking(user_id, org_id, dto)
932            .await
933            .unwrap();
934
935        // Try cancelling with a different user who is not registered as owner
936        let other_user = Uuid::new_v4();
937        let result = use_cases
938            .cancel_booking(created.id, other_user, org_id)
939            .await;
940        assert!(result.is_err());
941        assert!(result.unwrap_err().contains("Owner not found"));
942    }
943
944    #[tokio::test]
945    async fn test_delete_booking_success() {
946        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
947        let dto = make_create_dto(building_id);
948
949        let created = use_cases
950            .create_booking(user_id, org_id, dto)
951            .await
952            .unwrap();
953
954        let result = use_cases.delete_booking(created.id, user_id, org_id).await;
955        assert!(result.is_ok());
956
957        // Confirm it's gone
958        let fetch = use_cases.get_booking(created.id).await;
959        assert!(fetch.is_err());
960    }
961
962    #[tokio::test]
963    async fn test_confirm_booking_success() {
964        let (use_cases, user_id, org_id, building_id, _booking_repo) = setup_use_cases();
965        let dto = make_create_dto(building_id);
966
967        let created = use_cases
968            .create_booking(user_id, org_id, dto)
969            .await
970            .unwrap();
971
972        // Manually confirm via the use case
973        let result = use_cases.confirm_booking(created.id).await;
974        assert!(result.is_ok());
975        let confirmed = result.unwrap();
976        assert_eq!(confirmed.status, BookingStatus::Confirmed);
977
978        // Now we can complete it
979        let completed = use_cases.complete_booking(created.id).await;
980        assert!(completed.is_ok());
981        assert_eq!(completed.unwrap().status, BookingStatus::Completed);
982    }
983
984    #[tokio::test]
985    async fn test_list_building_bookings() {
986        let (use_cases, user_id, org_id, building_id, _) = setup_use_cases();
987
988        let dto1 = make_create_dto(building_id);
989
990        let mut dto2 = make_create_dto(building_id);
991        dto2.resource_name = "Meeting Room B".to_string();
992
993        use_cases
994            .create_booking(user_id, org_id, dto1)
995            .await
996            .unwrap();
997        use_cases
998            .create_booking(user_id, org_id, dto2)
999            .await
1000            .unwrap();
1001
1002        let result = use_cases.list_building_bookings(building_id).await;
1003        assert!(result.is_ok());
1004        assert_eq!(result.unwrap().len(), 2);
1005    }
1006
1007    #[tokio::test]
1008    async fn test_owner_not_found_for_user() {
1009        let booking_repo = Arc::new(MockBookingRepo::new());
1010        let owner_repo = Arc::new(MockOwnerRepo::new());
1011        // Do not add any owner
1012        let use_cases = ResourceBookingUseCases::new(
1013            booking_repo as Arc<dyn ResourceBookingRepository>,
1014            owner_repo as Arc<dyn OwnerRepository>,
1015        );
1016
1017        let building_id = Uuid::new_v4();
1018        let dto = make_create_dto(building_id);
1019        let result = use_cases
1020            .create_booking(Uuid::new_v4(), Uuid::new_v4(), dto)
1021            .await;
1022        assert!(result.is_err());
1023        assert!(result.unwrap_err().contains("Owner not found"));
1024    }
1025}