Skip to main content

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    /// Story #588 (INV-5/FR27) — le syndic réserve pour le compte de l'ACP
27    /// (AG, prestataires) plutôt qu'à titre personnel. Réservé au syndic
28    /// (RBAC en use-case) ; exige `motif`.
29    #[serde(default)]
30    pub on_behalf_of_acp: bool,
31    /// Obligatoire si `on_behalf_of_acp = true` (422 sinon).
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub motif: Option<String>,
34}
35
36/// DTO for updating booking details (resource_name, notes)
37///
38/// Time changes require cancellation and rebooking to ensure conflict detection.
39#[derive(Debug, Serialize, Deserialize, Clone)]
40pub struct UpdateResourceBookingDto {
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub resource_name: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub notes: Option<String>,
45}
46
47/// Response DTO for ResourceBooking with enriched data
48#[derive(Debug, Serialize, Deserialize, Clone)]
49pub struct ResourceBookingResponseDto {
50    pub id: Uuid,
51    pub building_id: Uuid,
52    pub resource_type: ResourceType,
53    pub resource_name: String,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub booked_by: Option<Uuid>,
56    pub booked_by_name: String, // Enriched: Owner full name, or syndic label if on_behalf_of_acp
57    /// Story #588 — syndic user_id si `on_behalf_of_acp = true`.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub booked_by_user_id: Option<Uuid>,
60    #[serde(default)]
61    pub on_behalf_of_acp: bool,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub motif: Option<String>,
64    pub start_time: DateTime<Utc>,
65    pub end_time: DateTime<Utc>,
66    pub status: BookingStatus,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub notes: Option<String>,
69    pub recurring_pattern: RecurringPattern,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub recurrence_end_date: Option<DateTime<Utc>>,
72    pub created_at: DateTime<Utc>,
73    pub updated_at: DateTime<Utc>,
74    // Computed fields
75    pub duration_hours: f64,
76    pub is_active: bool,
77    pub is_past: bool,
78    pub is_future: bool,
79    pub is_modifiable: bool,
80    pub is_recurring: bool,
81}
82
83impl ResourceBookingResponseDto {
84    /// Convert ResourceBooking entity to response DTO with owner name enrichment
85    pub fn from_entity(booking: ResourceBooking, booked_by_name: String) -> Self {
86        Self {
87            id: booking.id,
88            building_id: booking.building_id,
89            resource_type: booking.resource_type.clone(),
90            resource_name: booking.resource_name.clone(),
91            booked_by: booking.booked_by,
92            booked_by_name,
93            booked_by_user_id: booking.booked_by_user_id,
94            on_behalf_of_acp: booking.on_behalf_of_acp,
95            motif: booking.motif.clone(),
96            start_time: booking.start_time,
97            end_time: booking.end_time,
98            status: booking.status.clone(),
99            notes: booking.notes.clone(),
100            recurring_pattern: booking.recurring_pattern.clone(),
101            recurrence_end_date: booking.recurrence_end_date,
102            created_at: booking.created_at,
103            updated_at: booking.updated_at,
104            // Computed fields
105            duration_hours: booking.duration_hours(),
106            is_active: booking.is_active(),
107            is_past: booking.is_past(),
108            is_future: booking.is_future(),
109            is_modifiable: booking.is_modifiable(),
110            is_recurring: booking.is_recurring(),
111        }
112    }
113}
114
115/// DTO for booking statistics
116#[derive(Debug, Serialize, Deserialize, Clone)]
117pub struct BookingStatisticsDto {
118    pub building_id: Uuid,
119    pub total_bookings: i64,
120    pub confirmed_bookings: i64,
121    pub pending_bookings: i64,
122    pub completed_bookings: i64,
123    pub cancelled_bookings: i64,
124    pub no_show_bookings: i64,
125    pub active_bookings: i64,                  // Currently in progress
126    pub upcoming_bookings: i64,                // Future bookings
127    pub total_hours_booked: f64,               // Sum of all booking durations
128    pub most_popular_resource: Option<String>, // Most booked resource name
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_create_dto_serialization() {
137        let dto = CreateResourceBookingDto {
138            building_id: Uuid::new_v4(),
139            resource_type: ResourceType::MeetingRoom,
140            resource_name: "Meeting Room A".to_string(),
141            start_time: Utc::now() + chrono::Duration::hours(2),
142            end_time: Utc::now() + chrono::Duration::hours(4),
143            notes: Some("Team meeting".to_string()),
144            recurring_pattern: RecurringPattern::None,
145            recurrence_end_date: None,
146            max_duration_hours: None,
147            max_advance_days: None,
148            on_behalf_of_acp: false,
149            motif: None,
150        };
151
152        let json = serde_json::to_string(&dto).unwrap();
153        assert!(json.contains("Meeting Room A"));
154    }
155
156    #[test]
157    fn test_response_dto_from_entity() {
158        let building_id = Uuid::new_v4();
159        let booked_by = Uuid::new_v4();
160        let start_time = Utc::now() + chrono::Duration::hours(2);
161        let end_time = start_time + chrono::Duration::hours(2);
162
163        let booking = ResourceBooking::new(
164            building_id,
165            ResourceType::MeetingRoom,
166            "Meeting Room A".to_string(),
167            booked_by,
168            start_time,
169            end_time,
170            Some("Team meeting".to_string()),
171            RecurringPattern::None,
172            None,
173            None,
174            None,
175        )
176        .unwrap();
177
178        let dto = ResourceBookingResponseDto::from_entity(booking, "John Doe".to_string());
179
180        assert_eq!(dto.resource_name, "Meeting Room A");
181        assert_eq!(dto.booked_by_name, "John Doe");
182        assert_eq!(dto.duration_hours, 2.0);
183        assert!(dto.is_future);
184        assert!(!dto.is_past);
185        assert!(dto.is_modifiable);
186    }
187}