koprogo_api/application/dto/
resource_booking_dto.rs

1use crate::domain::entities::{BookingStatus, RecurringPattern, ResourceBooking, ResourceType};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// DTO for creating a new resource booking
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct CreateResourceBookingDto {
9    pub building_id: Uuid,
10    pub resource_type: ResourceType,
11    pub resource_name: String, // e.g., "Meeting Room A", "Laundry Room 1st Floor"
12    pub start_time: DateTime<Utc>,
13    pub end_time: DateTime<Utc>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub notes: Option<String>,
16    #[serde(default)]
17    pub recurring_pattern: RecurringPattern, // Defaults to None
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub recurrence_end_date: Option<DateTime<Utc>>,
20    /// Optional max duration in hours (uses default if not provided)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub max_duration_hours: Option<i64>,
23    /// Optional max advance booking in days (uses default if not provided)
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub max_advance_days: Option<i64>,
26}
27
28/// DTO for updating booking details (resource_name, notes)
29///
30/// Time changes require cancellation and rebooking to ensure conflict detection.
31#[derive(Debug, Serialize, Deserialize, Clone)]
32pub struct UpdateResourceBookingDto {
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub resource_name: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub notes: Option<String>,
37}
38
39/// Response DTO for ResourceBooking with enriched data
40#[derive(Debug, Serialize, Deserialize, Clone)]
41pub struct ResourceBookingResponseDto {
42    pub id: Uuid,
43    pub building_id: Uuid,
44    pub resource_type: ResourceType,
45    pub resource_name: String,
46    pub booked_by: Uuid,
47    pub booked_by_name: String, // Enriched: Owner full name
48    pub start_time: DateTime<Utc>,
49    pub end_time: DateTime<Utc>,
50    pub status: BookingStatus,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub notes: Option<String>,
53    pub recurring_pattern: RecurringPattern,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub recurrence_end_date: Option<DateTime<Utc>>,
56    pub created_at: DateTime<Utc>,
57    pub updated_at: DateTime<Utc>,
58    // Computed fields
59    pub duration_hours: f64,
60    pub is_active: bool,
61    pub is_past: bool,
62    pub is_future: bool,
63    pub is_modifiable: bool,
64    pub is_recurring: bool,
65}
66
67impl ResourceBookingResponseDto {
68    /// Convert ResourceBooking entity to response DTO with owner name enrichment
69    pub fn from_entity(booking: ResourceBooking, booked_by_name: String) -> Self {
70        Self {
71            id: booking.id,
72            building_id: booking.building_id,
73            resource_type: booking.resource_type.clone(),
74            resource_name: booking.resource_name.clone(),
75            booked_by: booking.booked_by,
76            booked_by_name,
77            start_time: booking.start_time,
78            end_time: booking.end_time,
79            status: booking.status.clone(),
80            notes: booking.notes.clone(),
81            recurring_pattern: booking.recurring_pattern.clone(),
82            recurrence_end_date: booking.recurrence_end_date,
83            created_at: booking.created_at,
84            updated_at: booking.updated_at,
85            // Computed fields
86            duration_hours: booking.duration_hours(),
87            is_active: booking.is_active(),
88            is_past: booking.is_past(),
89            is_future: booking.is_future(),
90            is_modifiable: booking.is_modifiable(),
91            is_recurring: booking.is_recurring(),
92        }
93    }
94}
95
96/// DTO for booking statistics
97#[derive(Debug, Serialize, Deserialize, Clone)]
98pub struct BookingStatisticsDto {
99    pub building_id: Uuid,
100    pub total_bookings: i64,
101    pub confirmed_bookings: i64,
102    pub pending_bookings: i64,
103    pub completed_bookings: i64,
104    pub cancelled_bookings: i64,
105    pub no_show_bookings: i64,
106    pub active_bookings: i64,                  // Currently in progress
107    pub upcoming_bookings: i64,                // Future bookings
108    pub total_hours_booked: f64,               // Sum of all booking durations
109    pub most_popular_resource: Option<String>, // Most booked resource name
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_create_dto_serialization() {
118        let dto = CreateResourceBookingDto {
119            building_id: Uuid::new_v4(),
120            resource_type: ResourceType::MeetingRoom,
121            resource_name: "Meeting Room A".to_string(),
122            start_time: Utc::now() + chrono::Duration::hours(2),
123            end_time: Utc::now() + chrono::Duration::hours(4),
124            notes: Some("Team meeting".to_string()),
125            recurring_pattern: RecurringPattern::None,
126            recurrence_end_date: None,
127            max_duration_hours: None,
128            max_advance_days: None,
129        };
130
131        let json = serde_json::to_string(&dto).unwrap();
132        assert!(json.contains("Meeting Room A"));
133    }
134
135    #[test]
136    fn test_response_dto_from_entity() {
137        let building_id = Uuid::new_v4();
138        let booked_by = Uuid::new_v4();
139        let start_time = Utc::now() + chrono::Duration::hours(2);
140        let end_time = start_time + chrono::Duration::hours(2);
141
142        let booking = ResourceBooking::new(
143            building_id,
144            ResourceType::MeetingRoom,
145            "Meeting Room A".to_string(),
146            booked_by,
147            start_time,
148            end_time,
149            Some("Team meeting".to_string()),
150            RecurringPattern::None,
151            None,
152            None,
153            None,
154        )
155        .unwrap();
156
157        let dto = ResourceBookingResponseDto::from_entity(booking, "John Doe".to_string());
158
159        assert_eq!(dto.resource_name, "Meeting Room A");
160        assert_eq!(dto.booked_by_name, "John Doe");
161        assert_eq!(dto.duration_hours, 2.0);
162        assert!(dto.is_future);
163        assert!(!dto.is_past);
164        assert!(dto.is_modifiable);
165    }
166}