1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum SharedObjectCategory {
8 Tools,
10 Books,
12 Electronics,
14 Sports,
16 Gardening,
18 Kitchen,
20 Baby,
22 Other,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
28pub enum ObjectCondition {
29 Excellent,
31 Good,
33 Fair,
35 Used,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SharedObject {
55 pub id: Uuid,
56 pub owner_id: Uuid,
57 pub building_id: Uuid,
58 pub object_category: SharedObjectCategory,
59 pub object_name: String,
60 pub description: String,
61 pub condition: ObjectCondition,
62 pub is_available: bool,
63 pub rental_credits_per_day: Option<i32>,
65 pub deposit_credits: Option<i32>,
67 pub borrowing_duration_days: Option<i32>,
69 pub current_borrower_id: Option<Uuid>,
71 pub borrowed_at: Option<DateTime<Utc>>,
73 pub due_back_at: Option<DateTime<Utc>>,
75 pub photos: Option<Vec<String>>,
77 pub location_details: Option<String>,
79 pub usage_instructions: Option<String>,
81 pub created_at: DateTime<Utc>,
82 pub updated_at: DateTime<Utc>,
83}
84
85impl SharedObject {
86 #[allow(clippy::too_many_arguments)]
95 pub fn new(
96 owner_id: Uuid,
97 building_id: Uuid,
98 object_category: SharedObjectCategory,
99 object_name: String,
100 description: String,
101 condition: ObjectCondition,
102 is_available: bool,
103 rental_credits_per_day: Option<i32>,
104 deposit_credits: Option<i32>,
105 borrowing_duration_days: Option<i32>,
106 photos: Option<Vec<String>>,
107 location_details: Option<String>,
108 usage_instructions: Option<String>,
109 ) -> Result<Self, String> {
110 if object_name.len() < 3 {
112 return Err("Object name must be at least 3 characters".to_string());
113 }
114 if object_name.len() > 100 {
115 return Err("Object name cannot exceed 100 characters".to_string());
116 }
117
118 if description.trim().is_empty() {
120 return Err("Description cannot be empty".to_string());
121 }
122 if description.len() > 1000 {
123 return Err("Description cannot exceed 1000 characters".to_string());
124 }
125
126 if let Some(rate) = rental_credits_per_day {
128 if rate < 0 {
129 return Err("Rental rate cannot be negative".to_string());
130 }
131 if rate > 20 {
132 return Err("Rental rate cannot exceed 20 credits per day".to_string());
133 }
134 }
135
136 if let Some(deposit) = deposit_credits {
138 if deposit < 0 {
139 return Err("Deposit cannot be negative".to_string());
140 }
141 if deposit > 100 {
142 return Err("Deposit cannot exceed 100 credits".to_string());
143 }
144 }
145
146 if let Some(duration) = borrowing_duration_days {
148 if duration < 1 {
149 return Err("Borrowing duration must be at least 1 day".to_string());
150 }
151 if duration > 90 {
152 return Err("Borrowing duration cannot exceed 90 days".to_string());
153 }
154 }
155
156 let now = Utc::now();
157
158 Ok(Self {
159 id: Uuid::new_v4(),
160 owner_id,
161 building_id,
162 object_category,
163 object_name,
164 description,
165 condition,
166 is_available,
167 rental_credits_per_day,
168 deposit_credits,
169 borrowing_duration_days,
170 current_borrower_id: None,
171 borrowed_at: None,
172 due_back_at: None,
173 photos,
174 location_details,
175 usage_instructions,
176 created_at: now,
177 updated_at: now,
178 })
179 }
180
181 #[allow(clippy::too_many_arguments)]
187 pub fn update(
188 &mut self,
189 object_name: Option<String>,
190 description: Option<String>,
191 condition: Option<ObjectCondition>,
192 is_available: Option<bool>,
193 rental_credits_per_day: Option<Option<i32>>,
194 deposit_credits: Option<Option<i32>>,
195 borrowing_duration_days: Option<Option<i32>>,
196 photos: Option<Option<Vec<String>>>,
197 location_details: Option<Option<String>>,
198 usage_instructions: Option<Option<String>>,
199 ) -> Result<(), String> {
200 if self.is_borrowed() {
202 return Err("Cannot update object while it is borrowed".to_string());
203 }
204
205 if let Some(name) = object_name {
207 if name.len() < 3 {
208 return Err("Object name must be at least 3 characters".to_string());
209 }
210 if name.len() > 100 {
211 return Err("Object name cannot exceed 100 characters".to_string());
212 }
213 self.object_name = name;
214 }
215
216 if let Some(desc) = description {
218 if desc.trim().is_empty() {
219 return Err("Description cannot be empty".to_string());
220 }
221 if desc.len() > 1000 {
222 return Err("Description cannot exceed 1000 characters".to_string());
223 }
224 self.description = desc;
225 }
226
227 if let Some(cond) = condition {
229 self.condition = cond;
230 }
231
232 if let Some(available) = is_available {
234 self.is_available = available;
235 }
236
237 if let Some(rate_opt) = rental_credits_per_day {
239 if let Some(rate) = rate_opt {
240 if rate < 0 {
241 return Err("Rental rate cannot be negative".to_string());
242 }
243 if rate > 20 {
244 return Err("Rental rate cannot exceed 20 credits per day".to_string());
245 }
246 }
247 self.rental_credits_per_day = rate_opt;
248 }
249
250 if let Some(deposit_opt) = deposit_credits {
252 if let Some(deposit) = deposit_opt {
253 if deposit < 0 {
254 return Err("Deposit cannot be negative".to_string());
255 }
256 if deposit > 100 {
257 return Err("Deposit cannot exceed 100 credits".to_string());
258 }
259 }
260 self.deposit_credits = deposit_opt;
261 }
262
263 if let Some(duration_opt) = borrowing_duration_days {
265 if let Some(duration) = duration_opt {
266 if duration < 1 {
267 return Err("Borrowing duration must be at least 1 day".to_string());
268 }
269 if duration > 90 {
270 return Err("Borrowing duration cannot exceed 90 days".to_string());
271 }
272 }
273 self.borrowing_duration_days = duration_opt;
274 }
275
276 if let Some(photos_opt) = photos {
278 self.photos = photos_opt;
279 }
280
281 if let Some(location_opt) = location_details {
283 self.location_details = location_opt;
284 }
285
286 if let Some(instructions_opt) = usage_instructions {
288 self.usage_instructions = instructions_opt;
289 }
290
291 self.updated_at = Utc::now();
292 Ok(())
293 }
294
295 pub fn mark_available(&mut self) -> Result<(), String> {
297 if self.is_borrowed() {
298 return Err("Cannot mark as available while borrowed".to_string());
299 }
300 self.is_available = true;
301 self.updated_at = Utc::now();
302 Ok(())
303 }
304
305 pub fn mark_unavailable(&mut self) {
307 self.is_available = false;
308 self.updated_at = Utc::now();
309 }
310
311 pub fn borrow(&mut self, borrower_id: Uuid, duration_days: Option<i32>) -> Result<(), String> {
317 if !self.is_available {
318 return Err("Object is not available for borrowing".to_string());
319 }
320
321 if self.is_borrowed() {
322 return Err("Object is already borrowed".to_string());
323 }
324
325 if borrower_id == self.owner_id {
326 return Err("Owner cannot borrow their own object".to_string());
327 }
328
329 let duration = duration_days.or(self.borrowing_duration_days).unwrap_or(7); if duration < 1 || duration > 90 {
332 return Err("Borrowing duration must be between 1 and 90 days".to_string());
333 }
334
335 let now = Utc::now();
336 let due_back = now + Duration::days(duration as i64);
337
338 self.current_borrower_id = Some(borrower_id);
339 self.borrowed_at = Some(now);
340 self.due_back_at = Some(due_back);
341 self.is_available = false;
342 self.updated_at = now;
343
344 Ok(())
345 }
346
347 pub fn return_object(&mut self, returner_id: Uuid) -> Result<(), String> {
353 if !self.is_borrowed() {
354 return Err("Object is not currently borrowed".to_string());
355 }
356
357 if self.current_borrower_id != Some(returner_id) {
358 return Err("Only borrower can return object".to_string());
359 }
360
361 self.current_borrower_id = None;
362 self.borrowed_at = None;
363 self.due_back_at = None;
364 self.is_available = true;
365 self.updated_at = Utc::now();
366
367 Ok(())
368 }
369
370 pub fn is_borrowed(&self) -> bool {
372 self.current_borrower_id.is_some()
373 }
374
375 pub fn is_free(&self) -> bool {
377 self.rental_credits_per_day.is_none() || self.rental_credits_per_day == Some(0)
378 }
379
380 pub fn is_overdue(&self) -> bool {
382 if let Some(due_back) = self.due_back_at {
383 Utc::now() > due_back
384 } else {
385 false
386 }
387 }
388
389 pub fn calculate_total_cost(&self) -> (i32, i32) {
393 let rental_cost =
394 if let (Some(borrowed), Some(rate)) = (self.borrowed_at, self.rental_credits_per_day) {
395 let days_borrowed = (Utc::now() - borrowed).num_days() + 1; (days_borrowed as i32) * rate
397 } else {
398 0
399 };
400
401 let deposit = self.deposit_credits.unwrap_or(0);
402
403 (rental_cost, deposit)
404 }
405
406 pub fn days_overdue(&self) -> i32 {
408 if let Some(due_back) = self.due_back_at {
409 let overdue_duration = Utc::now() - due_back;
410 if overdue_duration.num_days() > 0 {
411 return overdue_duration.num_days() as i32;
412 }
413 }
414 0
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_create_shared_object_success() {
424 let owner_id = Uuid::new_v4();
425 let building_id = Uuid::new_v4();
426
427 let object = SharedObject::new(
428 owner_id,
429 building_id,
430 SharedObjectCategory::Tools,
431 "Power Drill".to_string(),
432 "18V cordless drill with battery".to_string(),
433 ObjectCondition::Good,
434 true,
435 Some(2), Some(10), Some(7), None,
439 Some("Basement storage room".to_string()),
440 Some("Charge battery before use".to_string()),
441 );
442
443 assert!(object.is_ok());
444 let object = object.unwrap();
445 assert_eq!(object.owner_id, owner_id);
446 assert_eq!(object.object_category, SharedObjectCategory::Tools);
447 assert!(object.is_available);
448 assert!(!object.is_free());
449 assert!(!object.is_borrowed());
450 }
451
452 #[test]
453 fn test_object_name_too_short_fails() {
454 let owner_id = Uuid::new_v4();
455 let building_id = Uuid::new_v4();
456
457 let result = SharedObject::new(
458 owner_id,
459 building_id,
460 SharedObjectCategory::Books,
461 "AB".to_string(), "Description".to_string(),
463 ObjectCondition::Excellent,
464 true,
465 None,
466 None,
467 None,
468 None,
469 None,
470 None,
471 );
472
473 assert!(result.is_err());
474 assert_eq!(
475 result.unwrap_err(),
476 "Object name must be at least 3 characters"
477 );
478 }
479
480 #[test]
481 fn test_rental_rate_exceeds_20_fails() {
482 let owner_id = Uuid::new_v4();
483 let building_id = Uuid::new_v4();
484
485 let result = SharedObject::new(
486 owner_id,
487 building_id,
488 SharedObjectCategory::Electronics,
489 "Projector".to_string(),
490 "HD projector".to_string(),
491 ObjectCondition::Excellent,
492 true,
493 Some(25), None,
495 None,
496 None,
497 None,
498 None,
499 );
500
501 assert!(result.is_err());
502 assert_eq!(
503 result.unwrap_err(),
504 "Rental rate cannot exceed 20 credits per day"
505 );
506 }
507
508 #[test]
509 fn test_borrow_object_success() {
510 let owner_id = Uuid::new_v4();
511 let building_id = Uuid::new_v4();
512 let borrower_id = Uuid::new_v4();
513
514 let mut object = SharedObject::new(
515 owner_id,
516 building_id,
517 SharedObjectCategory::Sports,
518 "Mountain Bike".to_string(),
519 "26 inch mountain bike".to_string(),
520 ObjectCondition::Good,
521 true,
522 Some(5),
523 Some(20),
524 Some(3), None,
526 None,
527 None,
528 )
529 .unwrap();
530
531 let result = object.borrow(borrower_id, Some(3));
532 assert!(result.is_ok());
533 assert!(object.is_borrowed());
534 assert!(!object.is_available);
535 assert_eq!(object.current_borrower_id, Some(borrower_id));
536 assert!(object.borrowed_at.is_some());
537 assert!(object.due_back_at.is_some());
538 }
539
540 #[test]
541 fn test_owner_cannot_borrow_own_object() {
542 let owner_id = Uuid::new_v4();
543 let building_id = Uuid::new_v4();
544
545 let mut object = SharedObject::new(
546 owner_id,
547 building_id,
548 SharedObjectCategory::Gardening,
549 "Lawn Mower".to_string(),
550 "Electric lawn mower".to_string(),
551 ObjectCondition::Excellent,
552 true,
553 Some(3),
554 None,
555 Some(1),
556 None,
557 None,
558 None,
559 )
560 .unwrap();
561
562 let result = object.borrow(owner_id, Some(1)); assert!(result.is_err());
564 assert_eq!(result.unwrap_err(), "Owner cannot borrow their own object");
565 }
566
567 #[test]
568 fn test_return_object_success() {
569 let owner_id = Uuid::new_v4();
570 let building_id = Uuid::new_v4();
571 let borrower_id = Uuid::new_v4();
572
573 let mut object = SharedObject::new(
574 owner_id,
575 building_id,
576 SharedObjectCategory::Kitchen,
577 "Mixer".to_string(),
578 "Stand mixer".to_string(),
579 ObjectCondition::Good,
580 true,
581 None, None,
583 Some(7),
584 None,
585 None,
586 None,
587 )
588 .unwrap();
589
590 object.borrow(borrower_id, Some(7)).unwrap();
591 assert!(object.is_borrowed());
592
593 let result = object.return_object(borrower_id);
594 assert!(result.is_ok());
595 assert!(!object.is_borrowed());
596 assert!(object.is_available);
597 assert_eq!(object.current_borrower_id, None);
598 }
599
600 #[test]
601 fn test_only_borrower_can_return() {
602 let owner_id = Uuid::new_v4();
603 let building_id = Uuid::new_v4();
604 let borrower_id = Uuid::new_v4();
605 let other_user_id = Uuid::new_v4();
606
607 let mut object = SharedObject::new(
608 owner_id,
609 building_id,
610 SharedObjectCategory::Baby,
611 "Stroller".to_string(),
612 "Baby stroller".to_string(),
613 ObjectCondition::Fair,
614 true,
615 None,
616 None,
617 Some(14),
618 None,
619 None,
620 None,
621 )
622 .unwrap();
623
624 object.borrow(borrower_id, Some(14)).unwrap();
625
626 let result = object.return_object(other_user_id); assert!(result.is_err());
628 assert_eq!(result.unwrap_err(), "Only borrower can return object");
629 }
630
631 #[test]
632 fn test_is_free() {
633 let owner_id = Uuid::new_v4();
634 let building_id = Uuid::new_v4();
635
636 let object1 = SharedObject::new(
638 owner_id,
639 building_id,
640 SharedObjectCategory::Books,
641 "Book Title".to_string(),
642 "Description".to_string(),
643 ObjectCondition::Good,
644 true,
645 None, None,
647 None,
648 None,
649 None,
650 None,
651 )
652 .unwrap();
653 assert!(object1.is_free());
654
655 let object2 = SharedObject::new(
657 owner_id,
658 building_id,
659 SharedObjectCategory::Books,
660 "Book Title".to_string(),
661 "Description".to_string(),
662 ObjectCondition::Good,
663 true,
664 Some(0), None,
666 None,
667 None,
668 None,
669 None,
670 )
671 .unwrap();
672 assert!(object2.is_free());
673
674 let object3 = SharedObject::new(
676 owner_id,
677 building_id,
678 SharedObjectCategory::Electronics,
679 "Camera".to_string(),
680 "DSLR camera".to_string(),
681 ObjectCondition::Excellent,
682 true,
683 Some(10),
684 None,
685 None,
686 None,
687 None,
688 None,
689 )
690 .unwrap();
691 assert!(!object3.is_free());
692 }
693
694 #[test]
695 fn test_calculate_total_cost() {
696 let owner_id = Uuid::new_v4();
697 let building_id = Uuid::new_v4();
698 let borrower_id = Uuid::new_v4();
699
700 let mut object = SharedObject::new(
701 owner_id,
702 building_id,
703 SharedObjectCategory::Tools,
704 "Chainsaw".to_string(),
705 "Gas chainsaw".to_string(),
706 ObjectCondition::Good,
707 true,
708 Some(5), Some(30), Some(3),
711 None,
712 None,
713 None,
714 )
715 .unwrap();
716
717 object.borrow(borrower_id, Some(3)).unwrap();
718
719 let (rental_cost, deposit) = object.calculate_total_cost();
720 assert_eq!(deposit, 30);
721 assert!(rental_cost >= 5); }
723
724 #[test]
725 fn test_cannot_update_while_borrowed() {
726 let owner_id = Uuid::new_v4();
727 let building_id = Uuid::new_v4();
728 let borrower_id = Uuid::new_v4();
729
730 let mut object = SharedObject::new(
731 owner_id,
732 building_id,
733 SharedObjectCategory::Other,
734 "Item".to_string(),
735 "Description".to_string(),
736 ObjectCondition::Good,
737 true,
738 None,
739 None,
740 Some(7),
741 None,
742 None,
743 None,
744 )
745 .unwrap();
746
747 object.borrow(borrower_id, Some(7)).unwrap();
748
749 let result = object.update(
750 Some("New Name".to_string()),
751 None,
752 None,
753 None,
754 None,
755 None,
756 None,
757 None,
758 None,
759 None,
760 );
761
762 assert!(result.is_err());
763 assert_eq!(
764 result.unwrap_err(),
765 "Cannot update object while it is borrowed"
766 );
767 }
768
769 #[test]
770 fn test_mark_available_while_borrowed_fails() {
771 let owner_id = Uuid::new_v4();
772 let building_id = Uuid::new_v4();
773 let borrower_id = Uuid::new_v4();
774
775 let mut object = SharedObject::new(
776 owner_id,
777 building_id,
778 SharedObjectCategory::Sports,
779 "Tennis Racket".to_string(),
780 "Professional racket".to_string(),
781 ObjectCondition::Excellent,
782 true,
783 Some(2),
784 None,
785 Some(7),
786 None,
787 None,
788 None,
789 )
790 .unwrap();
791
792 object.borrow(borrower_id, Some(7)).unwrap();
793
794 let result = object.mark_available();
795 assert!(result.is_err());
796 assert_eq!(
797 result.unwrap_err(),
798 "Cannot mark as available while borrowed"
799 );
800 }
801}