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