1use crate::application::dto::{
2 BorrowObjectDto, CategoryObjectCount, CreateSharedObjectDto, SharedObjectResponseDto,
3 SharedObjectStatisticsDto, SharedObjectSummaryDto, UpdateSharedObjectDto,
4};
5use crate::application::ports::{
6 OwnerCreditBalanceRepository, OwnerRepository, SharedObjectRepository,
7};
8use crate::domain::entities::{SharedObject, SharedObjectCategory};
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub struct SharedObjectUseCases {
13 shared_object_repo: Arc<dyn SharedObjectRepository>,
14 owner_repo: Arc<dyn OwnerRepository>,
15 credit_balance_repo: Arc<dyn OwnerCreditBalanceRepository>,
16}
17
18impl SharedObjectUseCases {
19 pub fn new(
20 shared_object_repo: Arc<dyn SharedObjectRepository>,
21 owner_repo: Arc<dyn OwnerRepository>,
22 credit_balance_repo: Arc<dyn OwnerCreditBalanceRepository>,
23 ) -> Self {
24 Self {
25 shared_object_repo,
26 owner_repo,
27 credit_balance_repo,
28 }
29 }
30
31 async fn resolve_owner(
33 &self,
34 user_id: Uuid,
35 organization_id: Uuid,
36 ) -> Result<crate::domain::entities::Owner, String> {
37 self.owner_repo
38 .find_by_user_id_and_organization(user_id, organization_id)
39 .await?
40 .ok_or_else(|| "Owner not found for this user in the organization".to_string())
41 }
42
43 pub async fn create_shared_object(
48 &self,
49 user_id: Uuid,
50 organization_id: Uuid,
51 dto: CreateSharedObjectDto,
52 ) -> Result<SharedObjectResponseDto, String> {
53 let owner = self.resolve_owner(user_id, organization_id).await?;
54 let owner_id = owner.id;
55
56 let object = SharedObject::new(
57 owner_id,
58 dto.building_id,
59 dto.object_category,
60 dto.object_name,
61 dto.description,
62 dto.condition,
63 dto.is_available,
64 dto.rental_credits_per_day,
65 dto.deposit_credits,
66 dto.borrowing_duration_days,
67 dto.photos,
68 dto.location_details,
69 dto.usage_instructions,
70 )?;
71
72 let created = self.shared_object_repo.create(&object).await?;
73
74 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
75 Ok(SharedObjectResponseDto::from_shared_object(
76 created, owner_name, None,
77 ))
78 }
79
80 pub async fn get_shared_object(
82 &self,
83 object_id: Uuid,
84 ) -> Result<SharedObjectResponseDto, String> {
85 let object = self
86 .shared_object_repo
87 .find_by_id(object_id)
88 .await?
89 .ok_or("Shared object not found".to_string())?;
90
91 let owner = self
92 .owner_repo
93 .find_by_id(object.owner_id)
94 .await?
95 .ok_or("Owner not found".to_string())?;
96 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
97
98 let borrower_name = if let Some(borrower_id) = object.current_borrower_id {
100 let borrower = self.owner_repo.find_by_id(borrower_id).await?;
101 borrower.map(|b| format!("{} {}", b.first_name, b.last_name))
102 } else {
103 None
104 };
105
106 Ok(SharedObjectResponseDto::from_shared_object(
107 object,
108 owner_name,
109 borrower_name,
110 ))
111 }
112
113 pub async fn list_building_objects(
118 &self,
119 building_id: Uuid,
120 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
121 let objects = self
122 .shared_object_repo
123 .find_by_building(building_id)
124 .await?;
125 self.enrich_objects_summary(objects).await
126 }
127
128 pub async fn list_available_objects(
133 &self,
134 building_id: Uuid,
135 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
136 let objects = self
137 .shared_object_repo
138 .find_available_by_building(building_id)
139 .await?;
140 self.enrich_objects_summary(objects).await
141 }
142
143 pub async fn list_borrowed_objects(
145 &self,
146 building_id: Uuid,
147 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
148 let objects = self
149 .shared_object_repo
150 .find_borrowed_by_building(building_id)
151 .await?;
152 self.enrich_objects_summary(objects).await
153 }
154
155 pub async fn list_overdue_objects(
157 &self,
158 building_id: Uuid,
159 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
160 let objects = self
161 .shared_object_repo
162 .find_overdue_by_building(building_id)
163 .await?;
164 self.enrich_objects_summary(objects).await
165 }
166
167 pub async fn list_owner_objects(
169 &self,
170 owner_id: Uuid,
171 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
172 let objects = self.shared_object_repo.find_by_owner(owner_id).await?;
173 self.enrich_objects_summary(objects).await
174 }
175
176 pub async fn list_user_borrowed_objects(
178 &self,
179 user_id: Uuid,
180 organization_id: Uuid,
181 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
182 let owner = self.resolve_owner(user_id, organization_id).await?;
183 let objects = self
184 .shared_object_repo
185 .find_borrowed_by_user(owner.id)
186 .await?;
187 self.enrich_objects_summary(objects).await
188 }
189
190 pub async fn list_objects_by_category(
192 &self,
193 building_id: Uuid,
194 category: SharedObjectCategory,
195 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
196 let objects = self
197 .shared_object_repo
198 .find_by_category(building_id, category)
199 .await?;
200 self.enrich_objects_summary(objects).await
201 }
202
203 pub async fn list_free_objects(
205 &self,
206 building_id: Uuid,
207 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
208 let objects = self
209 .shared_object_repo
210 .find_free_by_building(building_id)
211 .await?;
212 self.enrich_objects_summary(objects).await
213 }
214
215 pub async fn update_shared_object(
221 &self,
222 object_id: Uuid,
223 user_id: Uuid,
224 organization_id: Uuid,
225 dto: UpdateSharedObjectDto,
226 ) -> Result<SharedObjectResponseDto, String> {
227 let owner = self.resolve_owner(user_id, organization_id).await?;
228 let mut object = self
229 .shared_object_repo
230 .find_by_id(object_id)
231 .await?
232 .ok_or("Shared object not found".to_string())?;
233
234 if object.owner_id != owner.id {
236 return Err("Unauthorized: only owner can update object".to_string());
237 }
238
239 object.update(
241 dto.object_name,
242 dto.description,
243 dto.condition,
244 dto.is_available,
245 dto.rental_credits_per_day,
246 dto.deposit_credits,
247 dto.borrowing_duration_days,
248 dto.photos,
249 dto.location_details,
250 dto.usage_instructions,
251 )?;
252
253 let updated = self.shared_object_repo.update(&object).await?;
255
256 self.get_shared_object(updated.id).await
258 }
259
260 pub async fn mark_object_available(
265 &self,
266 object_id: Uuid,
267 user_id: Uuid,
268 organization_id: Uuid,
269 ) -> Result<SharedObjectResponseDto, String> {
270 let owner = self.resolve_owner(user_id, organization_id).await?;
271 let mut object = self
272 .shared_object_repo
273 .find_by_id(object_id)
274 .await?
275 .ok_or("Shared object not found".to_string())?;
276
277 if object.owner_id != owner.id {
279 return Err("Unauthorized: only owner can mark object as available".to_string());
280 }
281
282 object.mark_available()?;
284
285 let updated = self.shared_object_repo.update(&object).await?;
287
288 self.get_shared_object(updated.id).await
290 }
291
292 pub async fn mark_object_unavailable(
297 &self,
298 object_id: Uuid,
299 user_id: Uuid,
300 organization_id: Uuid,
301 ) -> Result<SharedObjectResponseDto, String> {
302 let owner = self.resolve_owner(user_id, organization_id).await?;
303 let mut object = self
304 .shared_object_repo
305 .find_by_id(object_id)
306 .await?
307 .ok_or("Shared object not found".to_string())?;
308
309 if object.owner_id != owner.id {
311 return Err("Unauthorized: only owner can mark object as unavailable".to_string());
312 }
313
314 object.mark_unavailable();
316
317 let updated = self.shared_object_repo.update(&object).await?;
319
320 self.get_shared_object(updated.id).await
322 }
323
324 pub async fn borrow_object(
334 &self,
335 object_id: Uuid,
336 user_id: Uuid,
337 organization_id: Uuid,
338 dto: BorrowObjectDto,
339 ) -> Result<SharedObjectResponseDto, String> {
340 let borrower = self.resolve_owner(user_id, organization_id).await?;
341 let borrower_id = borrower.id;
342 let mut object = self
343 .shared_object_repo
344 .find_by_id(object_id)
345 .await?
346 .ok_or("Shared object not found".to_string())?;
347
348 if let Some(deposit_amount) = object.deposit_credits {
350 if deposit_amount > 0 {
351 let mut borrower_balance = self
353 .credit_balance_repo
354 .get_or_create(borrower_id, object.building_id)
355 .await?;
356
357 let new_balance = borrower_balance.balance - deposit_amount;
360 if new_balance < -100 {
361 return Err(format!(
363 "Insufficient credit balance. Deposit required: {} credits. \
364 Your balance: {} credits. Trust limit: -100 credits.",
365 deposit_amount, borrower_balance.balance
366 ));
367 }
368
369 borrower_balance.spend_credits(deposit_amount)?;
371
372 self.credit_balance_repo.update(&borrower_balance).await?;
374 }
375 }
376
377 object.borrow(borrower_id, dto.duration_days)?;
379
380 let updated = self.shared_object_repo.update(&object).await?;
382
383 self.get_shared_object(updated.id).await
385 }
386
387 pub async fn return_object(
397 &self,
398 object_id: Uuid,
399 user_id: Uuid,
400 organization_id: Uuid,
401 ) -> Result<SharedObjectResponseDto, String> {
402 let returner = self.resolve_owner(user_id, organization_id).await?;
403 let returner_id = returner.id;
404 let mut object = self
405 .shared_object_repo
406 .find_by_id(object_id)
407 .await?
408 .ok_or("Shared object not found".to_string())?;
409
410 let (rental_cost, deposit) = object.calculate_total_cost();
412
413 if rental_cost > 0 || deposit > 0 {
414 let borrower_id = object
415 .current_borrower_id
416 .ok_or("No current borrower to refund".to_string())?;
417 let owner_id = object.owner_id;
418
419 let mut borrower_balance = self
421 .credit_balance_repo
422 .get_or_create(borrower_id, object.building_id)
423 .await?;
424
425 let mut owner_balance = self
427 .credit_balance_repo
428 .get_or_create(owner_id, object.building_id)
429 .await?;
430
431 if rental_cost > 0 {
433 borrower_balance.spend_credits(rental_cost)?;
434 owner_balance.earn_credits(rental_cost)?;
435 }
436
437 if deposit > 0 {
439 borrower_balance.earn_credits(deposit)?;
440 }
441
442 self.credit_balance_repo.update(&borrower_balance).await?;
444 self.credit_balance_repo.update(&owner_balance).await?;
445 }
446
447 object.return_object(returner_id)?;
449
450 let updated = self.shared_object_repo.update(&object).await?;
452
453 self.get_shared_object(updated.id).await
455 }
456
457 pub async fn delete_shared_object(
463 &self,
464 object_id: Uuid,
465 user_id: Uuid,
466 organization_id: Uuid,
467 ) -> Result<(), String> {
468 let owner = self.resolve_owner(user_id, organization_id).await?;
469 let object = self
470 .shared_object_repo
471 .find_by_id(object_id)
472 .await?
473 .ok_or("Shared object not found".to_string())?;
474
475 if object.owner_id != owner.id {
477 return Err("Unauthorized: only owner can delete object".to_string());
478 }
479
480 if object.is_borrowed() {
482 return Err("Cannot delete object while it is borrowed".to_string());
483 }
484
485 self.shared_object_repo.delete(object_id).await?;
487
488 Ok(())
489 }
490
491 pub async fn get_object_statistics(
493 &self,
494 building_id: Uuid,
495 ) -> Result<SharedObjectStatisticsDto, String> {
496 let total_objects = self
497 .shared_object_repo
498 .count_by_building(building_id)
499 .await?;
500 let available_objects = self
501 .shared_object_repo
502 .count_available_by_building(building_id)
503 .await?;
504 let borrowed_objects = self
505 .shared_object_repo
506 .count_borrowed_by_building(building_id)
507 .await?;
508 let overdue_objects = self
509 .shared_object_repo
510 .count_overdue_by_building(building_id)
511 .await?;
512
513 let objects = self
515 .shared_object_repo
516 .find_by_building(building_id)
517 .await?;
518 let free_objects = objects.iter().filter(|o| o.is_free()).count() as i64;
519 let paid_objects = total_objects - free_objects;
520
521 let mut objects_by_category = Vec::new();
523 for category in [
524 SharedObjectCategory::Tools,
525 SharedObjectCategory::Books,
526 SharedObjectCategory::Electronics,
527 SharedObjectCategory::Sports,
528 SharedObjectCategory::Gardening,
529 SharedObjectCategory::Kitchen,
530 SharedObjectCategory::Baby,
531 SharedObjectCategory::Other,
532 ] {
533 let count = self
534 .shared_object_repo
535 .count_by_category(building_id, category.clone())
536 .await?;
537 if count > 0 {
538 objects_by_category.push(CategoryObjectCount { category, count });
539 }
540 }
541
542 Ok(SharedObjectStatisticsDto {
543 total_objects,
544 available_objects,
545 borrowed_objects,
546 overdue_objects,
547 free_objects,
548 paid_objects,
549 objects_by_category,
550 })
551 }
552
553 async fn enrich_objects_summary(
555 &self,
556 objects: Vec<SharedObject>,
557 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
558 let mut enriched = Vec::new();
559
560 for object in objects {
561 let owner = self.owner_repo.find_by_id(object.owner_id).await?;
563 let owner_name = if let Some(owner) = owner {
564 format!("{} {}", owner.first_name, owner.last_name)
565 } else {
566 "Unknown Owner".to_string()
567 };
568
569 enriched.push(SharedObjectSummaryDto::from_shared_object(
570 object, owner_name,
571 ));
572 }
573
574 Ok(enriched)
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use crate::application::dto::{OwnerFilters, PageRequest};
582 use crate::application::ports::{
583 OwnerCreditBalanceRepository, OwnerRepository, SharedObjectRepository,
584 };
585 use crate::domain::entities::{
586 ObjectCondition, Owner, OwnerCreditBalance, SharedObject, SharedObjectCategory,
587 };
588 use async_trait::async_trait;
589 use std::collections::HashMap;
590 use std::sync::{Arc, Mutex};
591 use uuid::Uuid;
592
593 struct MockSharedObjectRepo {
595 objects: Mutex<HashMap<Uuid, SharedObject>>,
596 }
597
598 impl MockSharedObjectRepo {
599 fn new() -> Self {
600 Self {
601 objects: Mutex::new(HashMap::new()),
602 }
603 }
604 }
605
606 #[async_trait]
607 impl SharedObjectRepository for MockSharedObjectRepo {
608 async fn create(&self, object: &SharedObject) -> Result<SharedObject, String> {
609 let mut map = self.objects.lock().unwrap();
610 map.insert(object.id, object.clone());
611 Ok(object.clone())
612 }
613
614 async fn find_by_id(&self, id: Uuid) -> Result<Option<SharedObject>, String> {
615 let map = self.objects.lock().unwrap();
616 Ok(map.get(&id).cloned())
617 }
618
619 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<SharedObject>, String> {
620 let map = self.objects.lock().unwrap();
621 Ok(map
622 .values()
623 .filter(|o| o.building_id == building_id)
624 .cloned()
625 .collect())
626 }
627
628 async fn find_available_by_building(
629 &self,
630 building_id: Uuid,
631 ) -> Result<Vec<SharedObject>, String> {
632 let map = self.objects.lock().unwrap();
633 Ok(map
634 .values()
635 .filter(|o| o.building_id == building_id && o.is_available)
636 .cloned()
637 .collect())
638 }
639
640 async fn find_borrowed_by_building(
641 &self,
642 building_id: Uuid,
643 ) -> Result<Vec<SharedObject>, String> {
644 let map = self.objects.lock().unwrap();
645 Ok(map
646 .values()
647 .filter(|o| o.building_id == building_id && o.is_borrowed())
648 .cloned()
649 .collect())
650 }
651
652 async fn find_overdue_by_building(
653 &self,
654 building_id: Uuid,
655 ) -> Result<Vec<SharedObject>, String> {
656 let map = self.objects.lock().unwrap();
657 Ok(map
658 .values()
659 .filter(|o| o.building_id == building_id && o.is_overdue())
660 .cloned()
661 .collect())
662 }
663
664 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<SharedObject>, String> {
665 let map = self.objects.lock().unwrap();
666 Ok(map
667 .values()
668 .filter(|o| o.owner_id == owner_id)
669 .cloned()
670 .collect())
671 }
672
673 async fn find_borrowed_by_user(
674 &self,
675 borrower_id: Uuid,
676 ) -> Result<Vec<SharedObject>, String> {
677 let map = self.objects.lock().unwrap();
678 Ok(map
679 .values()
680 .filter(|o| o.current_borrower_id == Some(borrower_id))
681 .cloned()
682 .collect())
683 }
684
685 async fn find_by_category(
686 &self,
687 building_id: Uuid,
688 category: SharedObjectCategory,
689 ) -> Result<Vec<SharedObject>, String> {
690 let map = self.objects.lock().unwrap();
691 Ok(map
692 .values()
693 .filter(|o| o.building_id == building_id && o.object_category == category)
694 .cloned()
695 .collect())
696 }
697
698 async fn find_free_by_building(
699 &self,
700 building_id: Uuid,
701 ) -> Result<Vec<SharedObject>, String> {
702 let map = self.objects.lock().unwrap();
703 Ok(map
704 .values()
705 .filter(|o| o.building_id == building_id && o.is_free())
706 .cloned()
707 .collect())
708 }
709
710 async fn update(&self, object: &SharedObject) -> Result<SharedObject, String> {
711 let mut map = self.objects.lock().unwrap();
712 map.insert(object.id, object.clone());
713 Ok(object.clone())
714 }
715
716 async fn delete(&self, id: Uuid) -> Result<(), String> {
717 let mut map = self.objects.lock().unwrap();
718 map.remove(&id);
719 Ok(())
720 }
721
722 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
723 let map = self.objects.lock().unwrap();
724 Ok(map
725 .values()
726 .filter(|o| o.building_id == building_id)
727 .count() as i64)
728 }
729
730 async fn count_available_by_building(&self, building_id: Uuid) -> Result<i64, String> {
731 let map = self.objects.lock().unwrap();
732 Ok(map
733 .values()
734 .filter(|o| o.building_id == building_id && o.is_available)
735 .count() as i64)
736 }
737
738 async fn count_borrowed_by_building(&self, building_id: Uuid) -> Result<i64, String> {
739 let map = self.objects.lock().unwrap();
740 Ok(map
741 .values()
742 .filter(|o| o.building_id == building_id && o.is_borrowed())
743 .count() as i64)
744 }
745
746 async fn count_overdue_by_building(&self, building_id: Uuid) -> Result<i64, String> {
747 let map = self.objects.lock().unwrap();
748 Ok(map
749 .values()
750 .filter(|o| o.building_id == building_id && o.is_overdue())
751 .count() as i64)
752 }
753
754 async fn count_by_category(
755 &self,
756 building_id: Uuid,
757 category: SharedObjectCategory,
758 ) -> Result<i64, String> {
759 let map = self.objects.lock().unwrap();
760 Ok(map
761 .values()
762 .filter(|o| o.building_id == building_id && o.object_category == category)
763 .count() as i64)
764 }
765 }
766
767 struct MockOwnerRepo {
769 owners: Mutex<HashMap<Uuid, Owner>>,
770 }
771
772 impl MockOwnerRepo {
773 fn new() -> Self {
774 Self {
775 owners: Mutex::new(HashMap::new()),
776 }
777 }
778 fn add_owner(&self, owner: Owner) {
779 self.owners.lock().unwrap().insert(owner.id, owner);
780 }
781 }
782
783 #[async_trait]
784 impl OwnerRepository for MockOwnerRepo {
785 async fn create(&self, owner: &Owner) -> Result<Owner, String> {
786 self.owners.lock().unwrap().insert(owner.id, owner.clone());
787 Ok(owner.clone())
788 }
789 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
790 Ok(self.owners.lock().unwrap().get(&id).cloned())
791 }
792 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
793 Ok(self
794 .owners
795 .lock()
796 .unwrap()
797 .values()
798 .find(|o| o.user_id == Some(user_id))
799 .cloned())
800 }
801 async fn find_by_user_id_and_organization(
802 &self,
803 user_id: Uuid,
804 org_id: Uuid,
805 ) -> Result<Option<Owner>, String> {
806 Ok(self
807 .owners
808 .lock()
809 .unwrap()
810 .values()
811 .find(|o| o.user_id == Some(user_id) && o.organization_id == org_id)
812 .cloned())
813 }
814 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
815 Ok(self
816 .owners
817 .lock()
818 .unwrap()
819 .values()
820 .find(|o| o.email == email)
821 .cloned())
822 }
823 async fn find_all(&self) -> Result<Vec<Owner>, String> {
824 Ok(self.owners.lock().unwrap().values().cloned().collect())
825 }
826 async fn find_all_paginated(
827 &self,
828 _p: &PageRequest,
829 _f: &OwnerFilters,
830 ) -> Result<(Vec<Owner>, i64), String> {
831 let all: Vec<_> = self.owners.lock().unwrap().values().cloned().collect();
832 let c = all.len() as i64;
833 Ok((all, c))
834 }
835 async fn update(&self, owner: &Owner) -> Result<Owner, String> {
836 self.owners.lock().unwrap().insert(owner.id, owner.clone());
837 Ok(owner.clone())
838 }
839 async fn delete(&self, id: Uuid) -> Result<bool, String> {
840 Ok(self.owners.lock().unwrap().remove(&id).is_some())
841 }
842 }
843
844 struct MockCreditBalanceRepo {
846 balances: Mutex<HashMap<(Uuid, Uuid), OwnerCreditBalance>>,
847 }
848
849 impl MockCreditBalanceRepo {
850 fn new() -> Self {
851 Self {
852 balances: Mutex::new(HashMap::new()),
853 }
854 }
855 }
856
857 #[async_trait]
858 impl OwnerCreditBalanceRepository for MockCreditBalanceRepo {
859 async fn create(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
860 let mut map = self.balances.lock().unwrap();
861 map.insert((balance.owner_id, balance.building_id), balance.clone());
862 Ok(balance.clone())
863 }
864 async fn find_by_owner_and_building(
865 &self,
866 owner_id: Uuid,
867 building_id: Uuid,
868 ) -> Result<Option<OwnerCreditBalance>, String> {
869 Ok(self
870 .balances
871 .lock()
872 .unwrap()
873 .get(&(owner_id, building_id))
874 .cloned())
875 }
876 async fn find_by_building(
877 &self,
878 building_id: Uuid,
879 ) -> Result<Vec<OwnerCreditBalance>, String> {
880 Ok(self
881 .balances
882 .lock()
883 .unwrap()
884 .values()
885 .filter(|b| b.building_id == building_id)
886 .cloned()
887 .collect())
888 }
889 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerCreditBalance>, String> {
890 Ok(self
891 .balances
892 .lock()
893 .unwrap()
894 .values()
895 .filter(|b| b.owner_id == owner_id)
896 .cloned()
897 .collect())
898 }
899 async fn get_or_create(
900 &self,
901 owner_id: Uuid,
902 building_id: Uuid,
903 ) -> Result<OwnerCreditBalance, String> {
904 let mut map = self.balances.lock().unwrap();
905 let key = (owner_id, building_id);
906 if let Some(existing) = map.get(&key) {
907 Ok(existing.clone())
908 } else {
909 let balance = OwnerCreditBalance::new(owner_id, building_id);
910 map.insert(key, balance.clone());
911 Ok(balance)
912 }
913 }
914 async fn update(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
915 let mut map = self.balances.lock().unwrap();
916 map.insert((balance.owner_id, balance.building_id), balance.clone());
917 Ok(balance.clone())
918 }
919 async fn delete(&self, owner_id: Uuid, building_id: Uuid) -> Result<bool, String> {
920 Ok(self
921 .balances
922 .lock()
923 .unwrap()
924 .remove(&(owner_id, building_id))
925 .is_some())
926 }
927 async fn get_leaderboard(
928 &self,
929 _building_id: Uuid,
930 _limit: i32,
931 ) -> Result<Vec<OwnerCreditBalance>, String> {
932 Ok(vec![])
933 }
934 async fn count_active_participants(&self, _building_id: Uuid) -> Result<i64, String> {
935 Ok(0)
936 }
937 async fn get_total_credits_in_circulation(
938 &self,
939 _building_id: Uuid,
940 ) -> Result<i32, String> {
941 Ok(0)
942 }
943 }
944
945 fn create_test_owner(user_id: Uuid, org_id: Uuid) -> Owner {
947 let mut owner = Owner::new(
948 org_id,
949 "Jean".to_string(),
950 "Dupont".to_string(),
951 "jean@test.com".to_string(),
952 None,
953 "Rue Test".to_string(),
954 "Brussels".to_string(),
955 "1000".to_string(),
956 "Belgium".to_string(),
957 )
958 .unwrap();
959 owner.user_id = Some(user_id);
960 owner
961 }
962
963 fn setup() -> (SharedObjectUseCases, Uuid, Uuid, Uuid, Uuid) {
964 let user_id = Uuid::new_v4();
965 let org_id = Uuid::new_v4();
966 let building_id = Uuid::new_v4();
967
968 let obj_repo = Arc::new(MockSharedObjectRepo::new());
969 let owner_repo = Arc::new(MockOwnerRepo::new());
970 let credit_repo = Arc::new(MockCreditBalanceRepo::new());
971
972 let owner = create_test_owner(user_id, org_id);
973 let owner_id = owner.id;
974 owner_repo.add_owner(owner);
975
976 let uc = SharedObjectUseCases::new(
977 obj_repo as Arc<dyn SharedObjectRepository>,
978 owner_repo as Arc<dyn OwnerRepository>,
979 credit_repo as Arc<dyn OwnerCreditBalanceRepository>,
980 );
981
982 (uc, user_id, org_id, building_id, owner_id)
983 }
984
985 fn make_create_dto(building_id: Uuid) -> CreateSharedObjectDto {
986 CreateSharedObjectDto {
987 building_id,
988 object_category: SharedObjectCategory::Tools,
989 object_name: "Power Drill".to_string(),
990 description: "18V cordless drill with battery".to_string(),
991 condition: ObjectCondition::Good,
992 is_available: true,
993 rental_credits_per_day: Some(2),
994 deposit_credits: Some(10),
995 borrowing_duration_days: Some(7),
996 photos: None,
997 location_details: Some("Basement".to_string()),
998 usage_instructions: None,
999 }
1000 }
1001
1002 #[tokio::test]
1005 async fn test_create_shared_object_success() {
1006 let (uc, user_id, org_id, building_id, _) = setup();
1007 let dto = make_create_dto(building_id);
1008 let result = uc.create_shared_object(user_id, org_id, dto).await;
1009 assert!(result.is_ok());
1010 let resp = result.unwrap();
1011 assert_eq!(resp.object_name, "Power Drill");
1012 assert_eq!(resp.owner_name, "Jean Dupont");
1013 }
1014
1015 #[tokio::test]
1016 async fn test_get_shared_object_success() {
1017 let (uc, user_id, org_id, building_id, _) = setup();
1018 let dto = make_create_dto(building_id);
1019 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1020
1021 let result = uc.get_shared_object(created.id).await;
1022 assert!(result.is_ok());
1023 assert_eq!(result.unwrap().id, created.id);
1024 }
1025
1026 #[tokio::test]
1027 async fn test_get_shared_object_not_found() {
1028 let (uc, _, _, _, _) = setup();
1029 let result = uc.get_shared_object(Uuid::new_v4()).await;
1030 assert!(result.is_err());
1031 assert_eq!(result.unwrap_err(), "Shared object not found");
1032 }
1033
1034 #[tokio::test]
1035 async fn test_delete_shared_object_success() {
1036 let (uc, user_id, org_id, building_id, _) = setup();
1037 let dto = make_create_dto(building_id);
1038 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1039
1040 let result = uc.delete_shared_object(created.id, user_id, org_id).await;
1041 assert!(result.is_ok());
1042 }
1043
1044 #[tokio::test]
1045 async fn test_delete_shared_object_wrong_owner() {
1046 let (uc, user_id, org_id, building_id, _) = setup();
1047 let dto = make_create_dto(building_id);
1048 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1049
1050 let other = Uuid::new_v4();
1052 let result = uc.delete_shared_object(created.id, other, org_id).await;
1053 assert!(result.is_err());
1054 assert!(result.unwrap_err().contains("Owner not found"));
1055 }
1056
1057 #[tokio::test]
1058 async fn test_list_building_objects() {
1059 let (uc, user_id, org_id, building_id, _) = setup();
1060
1061 let dto1 = make_create_dto(building_id);
1062 let mut dto2 = make_create_dto(building_id);
1063 dto2.object_name = "Hammer".to_string();
1064 dto2.description = "Steel hammer for nails".to_string();
1065
1066 uc.create_shared_object(user_id, org_id, dto1)
1067 .await
1068 .unwrap();
1069 uc.create_shared_object(user_id, org_id, dto2)
1070 .await
1071 .unwrap();
1072
1073 let result = uc.list_building_objects(building_id).await;
1074 assert!(result.is_ok());
1075 assert_eq!(result.unwrap().len(), 2);
1076 }
1077
1078 #[tokio::test]
1079 async fn test_owner_not_found() {
1080 let obj_repo = Arc::new(MockSharedObjectRepo::new());
1081 let owner_repo = Arc::new(MockOwnerRepo::new());
1082 let credit_repo = Arc::new(MockCreditBalanceRepo::new());
1083 let uc = SharedObjectUseCases::new(
1086 obj_repo as Arc<dyn SharedObjectRepository>,
1087 owner_repo as Arc<dyn OwnerRepository>,
1088 credit_repo as Arc<dyn OwnerCreditBalanceRepository>,
1089 );
1090
1091 let dto = make_create_dto(Uuid::new_v4());
1092 let result = uc
1093 .create_shared_object(Uuid::new_v4(), Uuid::new_v4(), dto)
1094 .await;
1095 assert!(result.is_err());
1096 assert!(result.unwrap_err().contains("Owner not found"));
1097 }
1098}