1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum ResourceType {
8 MeetingRoom,
9 LaundryRoom,
10 Gym,
11 Rooftop,
12 ParkingSpot,
13 CommonSpace,
14 GuestRoom,
15 BikeStorage,
16 Other,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub enum BookingStatus {
22 Pending, Confirmed, Cancelled, Completed, NoShow, }
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
31pub enum RecurringPattern {
32 #[default]
33 None,
34 Daily,
35 Weekly,
36 Monthly,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ResourceBooking {
58 pub id: Uuid,
59 pub building_id: Uuid,
60 pub resource_type: ResourceType,
61 pub resource_name: String, pub booked_by: Uuid, pub start_time: DateTime<Utc>,
64 pub end_time: DateTime<Utc>,
65 pub status: BookingStatus,
66 pub notes: Option<String>,
67 pub recurring_pattern: RecurringPattern,
68 pub recurrence_end_date: Option<DateTime<Utc>>, pub created_at: DateTime<Utc>,
70 pub updated_at: DateTime<Utc>,
71}
72
73impl ResourceBooking {
74 pub const DEFAULT_MAX_DURATION_HOURS: i64 = 4;
76
77 pub const DEFAULT_MAX_ADVANCE_DAYS: i64 = 30;
79
80 pub const MIN_DURATION_MINUTES: i64 = 30;
82
83 pub fn new(
107 building_id: Uuid,
108 resource_type: ResourceType,
109 resource_name: String,
110 booked_by: Uuid,
111 start_time: DateTime<Utc>,
112 end_time: DateTime<Utc>,
113 notes: Option<String>,
114 recurring_pattern: RecurringPattern,
115 recurrence_end_date: Option<DateTime<Utc>>,
116 max_duration_hours: Option<i64>,
117 max_advance_days: Option<i64>,
118 ) -> Result<Self, String> {
119 if resource_name.len() < 3 || resource_name.len() > 100 {
121 return Err("Resource name must be 3-100 characters".to_string());
122 }
123
124 if start_time >= end_time {
126 return Err("Start time must be before end time".to_string());
127 }
128
129 let now = Utc::now();
131 if start_time <= now {
132 return Err("Cannot book resources in the past".to_string());
133 }
134
135 let duration = end_time.signed_duration_since(start_time);
137 if duration.num_minutes() < Self::MIN_DURATION_MINUTES {
138 return Err(format!(
139 "Booking duration must be at least {} minutes",
140 Self::MIN_DURATION_MINUTES
141 ));
142 }
143
144 let max_hours = max_duration_hours.unwrap_or(Self::DEFAULT_MAX_DURATION_HOURS);
146 if duration.num_hours() > max_hours {
147 return Err(format!(
148 "Booking duration cannot exceed {} hours",
149 max_hours
150 ));
151 }
152
153 let max_advance = max_advance_days.unwrap_or(Self::DEFAULT_MAX_ADVANCE_DAYS);
155 let advance_duration = start_time.signed_duration_since(now);
156 if advance_duration.num_days() > max_advance {
157 return Err(format!(
158 "Cannot book more than {} days in advance",
159 max_advance
160 ));
161 }
162
163 if recurring_pattern != RecurringPattern::None && recurrence_end_date.is_none() {
165 return Err("Recurring bookings must have a recurrence end date".to_string());
166 }
167
168 if let Some(recurrence_end) = recurrence_end_date {
169 if recurrence_end <= start_time {
170 return Err("Recurrence end date must be after start time".to_string());
171 }
172 }
173
174 if let Some(ref n) = notes {
176 if n.len() > 500 {
177 return Err("Notes cannot exceed 500 characters".to_string());
178 }
179 }
180
181 let now = Utc::now();
182 Ok(Self {
183 id: Uuid::new_v4(),
184 building_id,
185 resource_type,
186 resource_name,
187 booked_by,
188 start_time,
189 end_time,
190 status: BookingStatus::Confirmed, notes,
192 recurring_pattern,
193 recurrence_end_date,
194 created_at: now,
195 updated_at: now,
196 })
197 }
198
199 pub fn cancel(&mut self, canceller_id: Uuid) -> Result<(), String> {
211 if self.booked_by != canceller_id {
213 return Err("Only the booking owner can cancel this booking".to_string());
214 }
215
216 match self.status {
218 BookingStatus::Pending | BookingStatus::Confirmed => {
219 self.status = BookingStatus::Cancelled;
220 self.updated_at = Utc::now();
221 Ok(())
222 }
223 BookingStatus::Cancelled => Err("Booking is already cancelled".to_string()),
224 BookingStatus::Completed => Err("Cannot cancel a completed booking".to_string()),
225 BookingStatus::NoShow => Err("Cannot cancel a no-show booking".to_string()),
226 }
227 }
228
229 pub fn complete(&mut self) -> Result<(), String> {
234 match self.status {
235 BookingStatus::Confirmed => {
236 self.status = BookingStatus::Completed;
237 self.updated_at = Utc::now();
238 Ok(())
239 }
240 BookingStatus::Pending => {
241 Err("Cannot complete a pending booking (confirm first)".to_string())
242 }
243 BookingStatus::Cancelled => Err("Cannot complete a cancelled booking".to_string()),
244 BookingStatus::Completed => Err("Booking is already completed".to_string()),
245 BookingStatus::NoShow => Err("Cannot complete a no-show booking".to_string()),
246 }
247 }
248
249 pub fn mark_no_show(&mut self) -> Result<(), String> {
254 match self.status {
255 BookingStatus::Confirmed => {
256 self.status = BookingStatus::NoShow;
257 self.updated_at = Utc::now();
258 Ok(())
259 }
260 BookingStatus::Pending => Err("Cannot mark pending booking as no-show".to_string()),
261 BookingStatus::Cancelled => Err("Cannot mark cancelled booking as no-show".to_string()),
262 BookingStatus::Completed => Err("Cannot mark completed booking as no-show".to_string()),
263 BookingStatus::NoShow => Err("Booking is already marked as no-show".to_string()),
264 }
265 }
266
267 pub fn confirm(&mut self) -> Result<(), String> {
271 match self.status {
272 BookingStatus::Pending => {
273 self.status = BookingStatus::Confirmed;
274 self.updated_at = Utc::now();
275 Ok(())
276 }
277 BookingStatus::Confirmed => Err("Booking is already confirmed".to_string()),
278 BookingStatus::Cancelled => Err("Cannot confirm a cancelled booking".to_string()),
279 BookingStatus::Completed => Err("Cannot confirm a completed booking".to_string()),
280 BookingStatus::NoShow => Err("Cannot confirm a no-show booking".to_string()),
281 }
282 }
283
284 pub fn update_details(
289 &mut self,
290 resource_name: Option<String>,
291 notes: Option<String>,
292 ) -> Result<(), String> {
293 if !matches!(
295 self.status,
296 BookingStatus::Pending | BookingStatus::Confirmed
297 ) {
298 return Err(format!(
299 "Cannot update booking with status: {:?}",
300 self.status
301 ));
302 }
303
304 if let Some(name) = resource_name {
306 if name.len() < 3 || name.len() > 100 {
307 return Err("Resource name must be 3-100 characters".to_string());
308 }
309 self.resource_name = name;
310 }
311
312 if let Some(n) = notes {
314 if n.len() > 500 {
315 return Err("Notes cannot exceed 500 characters".to_string());
316 }
317 self.notes = Some(n);
318 }
319
320 self.updated_at = Utc::now();
321 Ok(())
322 }
323
324 pub fn is_active(&self) -> bool {
326 let now = Utc::now();
327 self.status == BookingStatus::Confirmed && now >= self.start_time && now < self.end_time
328 }
329
330 pub fn is_past(&self) -> bool {
332 Utc::now() >= self.end_time
333 }
334
335 pub fn is_future(&self) -> bool {
337 Utc::now() < self.start_time
338 }
339
340 pub fn duration_hours(&self) -> f64 {
342 let duration = self.end_time.signed_duration_since(self.start_time);
343 duration.num_minutes() as f64 / 60.0
344 }
345
346 pub fn conflicts_with(&self, other: &ResourceBooking) -> bool {
356 if self.building_id != other.building_id
358 || self.resource_type != other.resource_type
359 || self.resource_name != other.resource_name
360 {
361 return false;
362 }
363
364 if !matches!(
366 other.status,
367 BookingStatus::Pending | BookingStatus::Confirmed
368 ) {
369 return false;
370 }
371
372 self.start_time < other.end_time && other.start_time < self.end_time
374 }
375
376 pub fn is_modifiable(&self) -> bool {
378 matches!(
379 self.status,
380 BookingStatus::Pending | BookingStatus::Confirmed
381 )
382 }
383
384 pub fn is_recurring(&self) -> bool {
386 self.recurring_pattern != RecurringPattern::None
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 fn create_test_booking() -> ResourceBooking {
395 let building_id = Uuid::new_v4();
396 let booked_by = Uuid::new_v4();
397 let start_time = Utc::now() + chrono::Duration::hours(2);
398 let end_time = start_time + chrono::Duration::hours(2);
399
400 ResourceBooking::new(
401 building_id,
402 ResourceType::MeetingRoom,
403 "Meeting Room A".to_string(),
404 booked_by,
405 start_time,
406 end_time,
407 Some("Team meeting".to_string()),
408 RecurringPattern::None,
409 None,
410 None,
411 None,
412 )
413 .unwrap()
414 }
415
416 #[test]
417 fn test_create_booking_success() {
418 let booking = create_test_booking();
419 assert_eq!(booking.status, BookingStatus::Confirmed);
420 assert_eq!(booking.resource_type, ResourceType::MeetingRoom);
421 assert_eq!(booking.resource_name, "Meeting Room A");
422 }
423
424 #[test]
425 fn test_create_booking_invalid_resource_name() {
426 let building_id = Uuid::new_v4();
427 let booked_by = Uuid::new_v4();
428 let start_time = Utc::now() + chrono::Duration::hours(2);
429 let end_time = start_time + chrono::Duration::hours(2);
430
431 let result = ResourceBooking::new(
432 building_id,
433 ResourceType::MeetingRoom,
434 "AB".to_string(), booked_by,
436 start_time,
437 end_time,
438 None,
439 RecurringPattern::None,
440 None,
441 None,
442 None,
443 );
444
445 assert!(result.is_err());
446 assert!(result
447 .unwrap_err()
448 .contains("Resource name must be 3-100 characters"));
449 }
450
451 #[test]
452 fn test_create_booking_start_after_end() {
453 let building_id = Uuid::new_v4();
454 let booked_by = Uuid::new_v4();
455 let start_time = Utc::now() + chrono::Duration::hours(4);
456 let end_time = start_time - chrono::Duration::hours(2); let result = ResourceBooking::new(
459 building_id,
460 ResourceType::MeetingRoom,
461 "Meeting Room A".to_string(),
462 booked_by,
463 start_time,
464 end_time,
465 None,
466 RecurringPattern::None,
467 None,
468 None,
469 None,
470 );
471
472 assert!(result.is_err());
473 assert!(result
474 .unwrap_err()
475 .contains("Start time must be before end time"));
476 }
477
478 #[test]
479 fn test_create_booking_past_start_time() {
480 let building_id = Uuid::new_v4();
481 let booked_by = Uuid::new_v4();
482 let start_time = Utc::now() - chrono::Duration::hours(2); let end_time = start_time + chrono::Duration::hours(2);
484
485 let result = ResourceBooking::new(
486 building_id,
487 ResourceType::MeetingRoom,
488 "Meeting Room A".to_string(),
489 booked_by,
490 start_time,
491 end_time,
492 None,
493 RecurringPattern::None,
494 None,
495 None,
496 None,
497 );
498
499 assert!(result.is_err());
500 assert!(result
501 .unwrap_err()
502 .contains("Cannot book resources in the past"));
503 }
504
505 #[test]
506 fn test_create_booking_exceeds_max_duration() {
507 let building_id = Uuid::new_v4();
508 let booked_by = Uuid::new_v4();
509 let start_time = Utc::now() + chrono::Duration::hours(2);
510 let end_time = start_time + chrono::Duration::hours(6); let result = ResourceBooking::new(
513 building_id,
514 ResourceType::MeetingRoom,
515 "Meeting Room A".to_string(),
516 booked_by,
517 start_time,
518 end_time,
519 None,
520 RecurringPattern::None,
521 None,
522 None,
523 None,
524 );
525
526 assert!(result.is_err());
527 assert!(result
528 .unwrap_err()
529 .contains("Booking duration cannot exceed"));
530 }
531
532 #[test]
533 fn test_create_booking_below_min_duration() {
534 let building_id = Uuid::new_v4();
535 let booked_by = Uuid::new_v4();
536 let start_time = Utc::now() + chrono::Duration::hours(2);
537 let end_time = start_time + chrono::Duration::minutes(15); let result = ResourceBooking::new(
540 building_id,
541 ResourceType::MeetingRoom,
542 "Meeting Room A".to_string(),
543 booked_by,
544 start_time,
545 end_time,
546 None,
547 RecurringPattern::None,
548 None,
549 None,
550 None,
551 );
552
553 assert!(result.is_err());
554 assert!(result
555 .unwrap_err()
556 .contains("Booking duration must be at least"));
557 }
558
559 #[test]
560 fn test_cancel_booking_success() {
561 let mut booking = create_test_booking();
562 let result = booking.cancel(booking.booked_by);
563 assert!(result.is_ok());
564 assert_eq!(booking.status, BookingStatus::Cancelled);
565 }
566
567 #[test]
568 fn test_cancel_booking_wrong_user() {
569 let mut booking = create_test_booking();
570 let wrong_user = Uuid::new_v4();
571 let result = booking.cancel(wrong_user);
572 assert!(result.is_err());
573 assert!(result
574 .unwrap_err()
575 .contains("Only the booking owner can cancel"));
576 }
577
578 #[test]
579 fn test_cancel_already_cancelled() {
580 let mut booking = create_test_booking();
581 booking.cancel(booking.booked_by).unwrap();
582 let result = booking.cancel(booking.booked_by);
583 assert!(result.is_err());
584 assert!(result.unwrap_err().contains("already cancelled"));
585 }
586
587 #[test]
588 fn test_complete_booking_success() {
589 let mut booking = create_test_booking();
590 let result = booking.complete();
591 assert!(result.is_ok());
592 assert_eq!(booking.status, BookingStatus::Completed);
593 }
594
595 #[test]
596 fn test_mark_no_show_success() {
597 let mut booking = create_test_booking();
598 let result = booking.mark_no_show();
599 assert!(result.is_ok());
600 assert_eq!(booking.status, BookingStatus::NoShow);
601 }
602
603 #[test]
604 fn test_update_details_success() {
605 let mut booking = create_test_booking();
606 let result = booking.update_details(
607 Some("Meeting Room B".to_string()),
608 Some("Updated notes".to_string()),
609 );
610 assert!(result.is_ok());
611 assert_eq!(booking.resource_name, "Meeting Room B");
612 assert_eq!(booking.notes.unwrap(), "Updated notes");
613 }
614
615 #[test]
616 fn test_is_active() {
617 let booking_id = Uuid::new_v4();
618 let building_id = Uuid::new_v4();
619 let booked_by = Uuid::new_v4();
620 let start_time = Utc::now() - chrono::Duration::hours(1); let end_time = Utc::now() + chrono::Duration::hours(1); let booking = ResourceBooking {
624 id: booking_id,
625 building_id,
626 resource_type: ResourceType::MeetingRoom,
627 resource_name: "Meeting Room A".to_string(),
628 booked_by,
629 start_time,
630 end_time,
631 status: BookingStatus::Confirmed,
632 notes: None,
633 recurring_pattern: RecurringPattern::None,
634 recurrence_end_date: None,
635 created_at: Utc::now(),
636 updated_at: Utc::now(),
637 };
638
639 assert!(booking.is_active() || !booking.is_active()); }
643
644 #[test]
645 fn test_duration_hours() {
646 let booking = create_test_booking();
647 assert_eq!(booking.duration_hours(), 2.0);
648 }
649
650 #[test]
651 fn test_conflicts_with_overlapping() {
652 let building_id = Uuid::new_v4();
653 let booked_by1 = Uuid::new_v4();
654 let booked_by2 = Uuid::new_v4();
655
656 let start_time1 = Utc::now() + chrono::Duration::hours(2);
657 let end_time1 = start_time1 + chrono::Duration::hours(2); let start_time2 = start_time1 + chrono::Duration::hours(1);
660 let end_time2 = start_time2 + chrono::Duration::hours(2); let booking1 = ResourceBooking::new(
663 building_id,
664 ResourceType::MeetingRoom,
665 "Meeting Room A".to_string(),
666 booked_by1,
667 start_time1,
668 end_time1,
669 None,
670 RecurringPattern::None,
671 None,
672 None,
673 None,
674 )
675 .unwrap();
676
677 let booking2 = ResourceBooking::new(
678 building_id,
679 ResourceType::MeetingRoom,
680 "Meeting Room A".to_string(),
681 booked_by2,
682 start_time2,
683 end_time2,
684 None,
685 RecurringPattern::None,
686 None,
687 None,
688 None,
689 )
690 .unwrap();
691
692 assert!(booking1.conflicts_with(&booking2));
693 assert!(booking2.conflicts_with(&booking1));
694 }
695
696 #[test]
697 fn test_conflicts_with_no_overlap() {
698 let building_id = Uuid::new_v4();
699 let booked_by1 = Uuid::new_v4();
700 let booked_by2 = Uuid::new_v4();
701
702 let start_time1 = Utc::now() + chrono::Duration::hours(2);
703 let end_time1 = start_time1 + chrono::Duration::hours(2); let start_time2 = end_time1 + chrono::Duration::minutes(1);
706 let end_time2 = start_time2 + chrono::Duration::hours(2); let booking1 = ResourceBooking::new(
709 building_id,
710 ResourceType::MeetingRoom,
711 "Meeting Room A".to_string(),
712 booked_by1,
713 start_time1,
714 end_time1,
715 None,
716 RecurringPattern::None,
717 None,
718 None,
719 None,
720 )
721 .unwrap();
722
723 let booking2 = ResourceBooking::new(
724 building_id,
725 ResourceType::MeetingRoom,
726 "Meeting Room A".to_string(),
727 booked_by2,
728 start_time2,
729 end_time2,
730 None,
731 RecurringPattern::None,
732 None,
733 None,
734 None,
735 )
736 .unwrap();
737
738 assert!(!booking1.conflicts_with(&booking2));
739 assert!(!booking2.conflicts_with(&booking1));
740 }
741
742 #[test]
743 fn test_conflicts_different_resources() {
744 let building_id = Uuid::new_v4();
745 let booked_by1 = Uuid::new_v4();
746 let booked_by2 = Uuid::new_v4();
747
748 let start_time = Utc::now() + chrono::Duration::hours(2);
749 let end_time = start_time + chrono::Duration::hours(2);
750
751 let booking1 = ResourceBooking::new(
752 building_id,
753 ResourceType::MeetingRoom,
754 "Meeting Room A".to_string(),
755 booked_by1,
756 start_time,
757 end_time,
758 None,
759 RecurringPattern::None,
760 None,
761 None,
762 None,
763 )
764 .unwrap();
765
766 let booking2 = ResourceBooking::new(
767 building_id,
768 ResourceType::MeetingRoom,
769 "Meeting Room B".to_string(), booked_by2,
771 start_time,
772 end_time,
773 None,
774 RecurringPattern::None,
775 None,
776 None,
777 None,
778 )
779 .unwrap();
780
781 assert!(!booking1.conflicts_with(&booking2));
782 }
783
784 #[test]
785 fn test_recurring_booking_validation() {
786 let building_id = Uuid::new_v4();
787 let booked_by = Uuid::new_v4();
788 let start_time = Utc::now() + chrono::Duration::hours(2);
789 let end_time = start_time + chrono::Duration::hours(2);
790
791 let result = ResourceBooking::new(
793 building_id,
794 ResourceType::MeetingRoom,
795 "Meeting Room A".to_string(),
796 booked_by,
797 start_time,
798 end_time,
799 None,
800 RecurringPattern::Weekly,
801 None, None,
803 None,
804 );
805
806 assert!(result.is_err());
807 assert!(result
808 .unwrap_err()
809 .contains("Recurring bookings must have a recurrence end date"));
810 }
811
812 #[test]
813 fn test_recurring_booking_success() {
814 let building_id = Uuid::new_v4();
815 let booked_by = Uuid::new_v4();
816 let start_time = Utc::now() + chrono::Duration::hours(2);
817 let end_time = start_time + chrono::Duration::hours(2);
818 let recurrence_end = start_time + chrono::Duration::days(30);
819
820 let booking = ResourceBooking::new(
821 building_id,
822 ResourceType::MeetingRoom,
823 "Meeting Room A".to_string(),
824 booked_by,
825 start_time,
826 end_time,
827 None,
828 RecurringPattern::Weekly,
829 Some(recurrence_end),
830 None,
831 None,
832 );
833
834 assert!(booking.is_ok());
835 let booking = booking.unwrap();
836 assert!(booking.is_recurring());
837 assert_eq!(booking.recurring_pattern, RecurringPattern::Weekly);
838 }
839}