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    /// Create a new resource booking with conflict detection
32    ///
33    /// # Steps
34    /// 1. Create booking entity (validates business rules)
35    /// 2. Check for conflicts (overlapping bookings for same resource)
36    /// 3. Persist booking
37    /// 4. Enrich response with owner name
38    ///
39    /// # Authorization
40    /// - Any authenticated owner can create bookings
41    pub async fn create_booking(
42        &self,
43        booked_by: Uuid,
44        dto: CreateResourceBookingDto,
45    ) -> Result<ResourceBookingResponseDto, String> {
46        // Create booking entity (validates business rules in constructor)
47        let booking = ResourceBooking::new(
48            dto.building_id,
49            dto.resource_type.clone(),
50            dto.resource_name.clone(),
51            booked_by,
52            dto.start_time,
53            dto.end_time,
54            dto.notes.clone(),
55            dto.recurring_pattern.clone(),
56            dto.recurrence_end_date,
57            dto.max_duration_hours,
58            dto.max_advance_days,
59        )?;
60
61        // Check for conflicts (overlapping bookings for same resource)
62        let conflicts = self
63            .booking_repo
64            .find_conflicts(
65                dto.building_id,
66                dto.resource_type,
67                &dto.resource_name,
68                dto.start_time,
69                dto.end_time,
70                None, // No exclusion for new bookings
71            )
72            .await?;
73
74        if !conflicts.is_empty() {
75            return Err(format!(
76                "Booking conflicts with {} existing booking(s) for this resource",
77                conflicts.len()
78            ));
79        }
80
81        // Persist booking
82        let created = self.booking_repo.create(&booking).await?;
83
84        // Enrich with owner name
85        self.enrich_booking_response(created).await
86    }
87
88    /// Get booking by ID with owner name enrichment
89    pub async fn get_booking(
90        &self,
91        booking_id: Uuid,
92    ) -> Result<ResourceBookingResponseDto, String> {
93        let booking = self
94            .booking_repo
95            .find_by_id(booking_id)
96            .await?
97            .ok_or("Booking not found".to_string())?;
98
99        self.enrich_booking_response(booking).await
100    }
101
102    /// List all bookings for a building
103    pub async fn list_building_bookings(
104        &self,
105        building_id: Uuid,
106    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
107        let bookings = self.booking_repo.find_by_building(building_id).await?;
108        self.enrich_bookings_response(bookings).await
109    }
110
111    /// List bookings by building and resource type
112    pub async fn list_by_resource_type(
113        &self,
114        building_id: Uuid,
115        resource_type: ResourceType,
116    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
117        let bookings = self
118            .booking_repo
119            .find_by_building_and_resource_type(building_id, resource_type)
120            .await?;
121        self.enrich_bookings_response(bookings).await
122    }
123
124    /// List bookings for a specific resource (building + type + name)
125    pub async fn list_by_resource(
126        &self,
127        building_id: Uuid,
128        resource_type: ResourceType,
129        resource_name: String,
130    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
131        let bookings = self
132            .booking_repo
133            .find_by_resource(building_id, resource_type, &resource_name)
134            .await?;
135        self.enrich_bookings_response(bookings).await
136    }
137
138    /// List bookings by user (owner who made the booking)
139    pub async fn list_user_bookings(
140        &self,
141        user_id: Uuid,
142    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
143        let bookings = self.booking_repo.find_by_user(user_id).await?;
144        self.enrich_bookings_response(bookings).await
145    }
146
147    /// List bookings by user and status
148    pub async fn list_user_bookings_by_status(
149        &self,
150        user_id: Uuid,
151        status: BookingStatus,
152    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
153        let bookings = self
154            .booking_repo
155            .find_by_user_and_status(user_id, status)
156            .await?;
157        self.enrich_bookings_response(bookings).await
158    }
159
160    /// List bookings by building and status
161    pub async fn list_building_bookings_by_status(
162        &self,
163        building_id: Uuid,
164        status: BookingStatus,
165    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
166        let bookings = self
167            .booking_repo
168            .find_by_building_and_status(building_id, status)
169            .await?;
170        self.enrich_bookings_response(bookings).await
171    }
172
173    /// List upcoming bookings (future, confirmed or pending)
174    pub async fn list_upcoming_bookings(
175        &self,
176        building_id: Uuid,
177        limit: Option<i64>,
178    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
179        let bookings = self.booking_repo.find_upcoming(building_id, limit).await?;
180        self.enrich_bookings_response(bookings).await
181    }
182
183    /// List active bookings (currently in progress)
184    pub async fn list_active_bookings(
185        &self,
186        building_id: Uuid,
187    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
188        let bookings = self.booking_repo.find_active(building_id).await?;
189        self.enrich_bookings_response(bookings).await
190    }
191
192    /// List past bookings
193    pub async fn list_past_bookings(
194        &self,
195        building_id: Uuid,
196        limit: Option<i64>,
197    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
198        let bookings = self.booking_repo.find_past(building_id, limit).await?;
199        self.enrich_bookings_response(bookings).await
200    }
201
202    /// Update booking details (resource_name, notes)
203    ///
204    /// Time changes require cancellation and rebooking to ensure conflict detection.
205    ///
206    /// # Authorization
207    /// - Only booking owner can update their booking
208    pub async fn update_booking(
209        &self,
210        booking_id: Uuid,
211        updater_id: Uuid,
212        dto: UpdateResourceBookingDto,
213    ) -> Result<ResourceBookingResponseDto, String> {
214        let mut booking = self
215            .booking_repo
216            .find_by_id(booking_id)
217            .await?
218            .ok_or("Booking not found".to_string())?;
219
220        // Authorization: Only booking owner can update
221        if booking.booked_by != updater_id {
222            return Err("Only the booking owner can update this booking".to_string());
223        }
224
225        // Update details (validates business rules)
226        booking.update_details(dto.resource_name, dto.notes)?;
227
228        // Persist changes
229        let updated = self.booking_repo.update(&booking).await?;
230
231        // Enrich with owner name
232        self.enrich_booking_response(updated).await
233    }
234
235    /// Cancel a booking
236    ///
237    /// # Authorization
238    /// - Only booking owner can cancel their booking
239    pub async fn cancel_booking(
240        &self,
241        booking_id: Uuid,
242        canceller_id: Uuid,
243    ) -> Result<ResourceBookingResponseDto, String> {
244        let mut booking = self
245            .booking_repo
246            .find_by_id(booking_id)
247            .await?
248            .ok_or("Booking not found".to_string())?;
249
250        // Cancel booking (validates authorization and state)
251        booking.cancel(canceller_id)?;
252
253        // Persist changes
254        let updated = self.booking_repo.update(&booking).await?;
255
256        // Enrich with owner name
257        self.enrich_booking_response(updated).await
258    }
259
260    /// Complete a booking (typically auto-called after end_time)
261    ///
262    /// # Authorization
263    /// - Admin only (for manual completion)
264    pub async fn complete_booking(
265        &self,
266        booking_id: Uuid,
267    ) -> Result<ResourceBookingResponseDto, String> {
268        let mut booking = self
269            .booking_repo
270            .find_by_id(booking_id)
271            .await?
272            .ok_or("Booking not found".to_string())?;
273
274        // Complete booking (validates state)
275        booking.complete()?;
276
277        // Persist changes
278        let updated = self.booking_repo.update(&booking).await?;
279
280        // Enrich with owner name
281        self.enrich_booking_response(updated).await
282    }
283
284    /// Mark booking as no-show
285    ///
286    /// # Authorization
287    /// - Admin only
288    pub async fn mark_no_show(
289        &self,
290        booking_id: Uuid,
291    ) -> Result<ResourceBookingResponseDto, String> {
292        let mut booking = self
293            .booking_repo
294            .find_by_id(booking_id)
295            .await?
296            .ok_or("Booking not found".to_string())?;
297
298        // Mark as no-show (validates state)
299        booking.mark_no_show()?;
300
301        // Persist changes
302        let updated = self.booking_repo.update(&booking).await?;
303
304        // Enrich with owner name
305        self.enrich_booking_response(updated).await
306    }
307
308    /// Confirm a pending booking
309    ///
310    /// # Authorization
311    /// - Admin only (for approval workflow)
312    pub async fn confirm_booking(
313        &self,
314        booking_id: Uuid,
315    ) -> Result<ResourceBookingResponseDto, String> {
316        let mut booking = self
317            .booking_repo
318            .find_by_id(booking_id)
319            .await?
320            .ok_or("Booking not found".to_string())?;
321
322        // Confirm booking (validates state)
323        booking.confirm()?;
324
325        // Persist changes
326        let updated = self.booking_repo.update(&booking).await?;
327
328        // Enrich with owner name
329        self.enrich_booking_response(updated).await
330    }
331
332    /// Delete a booking
333    ///
334    /// # Authorization
335    /// - Only booking owner can delete their booking
336    /// - Or admin for any booking
337    pub async fn delete_booking(&self, booking_id: Uuid, deleter_id: Uuid) -> Result<(), String> {
338        let booking = self
339            .booking_repo
340            .find_by_id(booking_id)
341            .await?
342            .ok_or("Booking not found".to_string())?;
343
344        // Authorization: Only booking owner can delete
345        if booking.booked_by != deleter_id {
346            return Err("Only the booking owner can delete this booking".to_string());
347        }
348
349        self.booking_repo.delete(booking_id).await
350    }
351
352    /// Check for booking conflicts
353    ///
354    /// Useful for frontend to preview conflicts before creating booking.
355    ///
356    /// Returns list of conflicting bookings with owner names.
357    pub async fn check_conflicts(
358        &self,
359        building_id: Uuid,
360        resource_type: ResourceType,
361        resource_name: String,
362        start_time: chrono::DateTime<Utc>,
363        end_time: chrono::DateTime<Utc>,
364        exclude_booking_id: Option<Uuid>,
365    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
366        let conflicts = self
367            .booking_repo
368            .find_conflicts(
369                building_id,
370                resource_type,
371                &resource_name,
372                start_time,
373                end_time,
374                exclude_booking_id,
375            )
376            .await?;
377
378        self.enrich_bookings_response(conflicts).await
379    }
380
381    /// Get booking statistics for a building
382    pub async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
383        self.booking_repo.get_statistics(building_id).await
384    }
385
386    /// Helper: Enrich single booking with owner name
387    async fn enrich_booking_response(
388        &self,
389        booking: ResourceBooking,
390    ) -> Result<ResourceBookingResponseDto, String> {
391        // Fetch owner to get full name
392        let owner = self
393            .owner_repo
394            .find_by_id(booking.booked_by)
395            .await?
396            .ok_or("Booking owner not found".to_string())?;
397
398        let booked_by_name = format!("{} {}", owner.first_name, owner.last_name);
399
400        Ok(ResourceBookingResponseDto::from_entity(
401            booking,
402            booked_by_name,
403        ))
404    }
405
406    /// Helper: Enrich multiple bookings with owner names
407    async fn enrich_bookings_response(
408        &self,
409        bookings: Vec<ResourceBooking>,
410    ) -> Result<Vec<ResourceBookingResponseDto>, String> {
411        let mut result = Vec::with_capacity(bookings.len());
412
413        for booking in bookings {
414            let enriched = self.enrich_booking_response(booking).await?;
415            result.push(enriched);
416        }
417
418        Ok(result)
419    }
420}