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(|| crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES.to_string())
56 }
57
58 pub async fn create_shared_object(
63 &self,
64 user_id: Uuid,
65 organization_id: Uuid,
66 dto: CreateSharedObjectDto,
67 ) -> Result<SharedObjectResponseDto, String> {
68 let owner = self.resolve_owner(user_id, organization_id).await?;
69 let owner_id = owner.id;
70
71 let object = SharedObject::new(
72 owner_id,
73 dto.building_id,
74 dto.object_category,
75 dto.object_name,
76 dto.description,
77 dto.condition,
78 dto.is_available,
79 dto.rental_credits_per_day,
80 dto.deposit_credits,
81 dto.borrowing_duration_days,
82 dto.photos,
83 dto.location_details,
84 dto.usage_instructions,
85 )?;
86
87 let created = self.shared_object_repo.create(&object).await?;
88
89 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
90 Ok(SharedObjectResponseDto::from_shared_object(
91 created, owner_name, None,
92 ))
93 }
94
95 pub async fn get_shared_object(
97 &self,
98 object_id: Uuid,
99 ) -> Result<SharedObjectResponseDto, String> {
100 let object = self
101 .shared_object_repo
102 .find_by_id(object_id)
103 .await?
104 .ok_or("Shared object not found".to_string())?;
105
106 let owner = self
107 .owner_repo
108 .find_by_id(object.owner_id)
109 .await?
110 .ok_or("Owner not found".to_string())?;
111 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
112
113 let borrower_name = if let Some(borrower_id) = object.current_borrower_id {
115 let borrower = self.owner_repo.find_by_id(borrower_id).await?;
116 borrower.map(|b| format!("{} {}", b.first_name, b.last_name))
117 } else {
118 None
119 };
120
121 Ok(SharedObjectResponseDto::from_shared_object(
122 object,
123 owner_name,
124 borrower_name,
125 ))
126 }
127
128 pub async fn list_building_objects(
133 &self,
134 building_id: Uuid,
135 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
136 let objects = self
137 .shared_object_repo
138 .find_by_building(building_id)
139 .await?;
140 self.enrich_objects_summary(objects).await
141 }
142
143 pub async fn list_available_objects(
148 &self,
149 building_id: Uuid,
150 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
151 let objects = self
152 .shared_object_repo
153 .find_available_by_building(building_id)
154 .await?;
155 self.enrich_objects_summary(objects).await
156 }
157
158 pub async fn list_borrowed_objects(
160 &self,
161 building_id: Uuid,
162 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
163 let objects = self
164 .shared_object_repo
165 .find_borrowed_by_building(building_id)
166 .await?;
167 self.enrich_objects_summary(objects).await
168 }
169
170 pub async fn list_overdue_objects(
172 &self,
173 building_id: Uuid,
174 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
175 let objects = self
176 .shared_object_repo
177 .find_overdue_by_building(building_id)
178 .await?;
179 self.enrich_objects_summary(objects).await
180 }
181
182 pub async fn list_owner_objects(
184 &self,
185 owner_id: Uuid,
186 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
187 let objects = self.shared_object_repo.find_by_owner(owner_id).await?;
188 self.enrich_objects_summary(objects).await
189 }
190
191 pub async fn list_user_borrowed_objects(
193 &self,
194 user_id: Uuid,
195 organization_id: Uuid,
196 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
197 let owner = self.resolve_owner(user_id, organization_id).await?;
198 let objects = self
199 .shared_object_repo
200 .find_borrowed_by_user(owner.id)
201 .await?;
202 self.enrich_objects_summary(objects).await
203 }
204
205 pub async fn list_objects_by_category(
207 &self,
208 building_id: Uuid,
209 category: SharedObjectCategory,
210 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
211 let objects = self
212 .shared_object_repo
213 .find_by_category(building_id, category)
214 .await?;
215 self.enrich_objects_summary(objects).await
216 }
217
218 pub async fn list_free_objects(
220 &self,
221 building_id: Uuid,
222 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
223 let objects = self
224 .shared_object_repo
225 .find_free_by_building(building_id)
226 .await?;
227 self.enrich_objects_summary(objects).await
228 }
229
230 pub async fn update_shared_object(
236 &self,
237 object_id: Uuid,
238 user_id: Uuid,
239 organization_id: Uuid,
240 dto: UpdateSharedObjectDto,
241 ) -> Result<SharedObjectResponseDto, String> {
242 let owner = self.resolve_owner(user_id, organization_id).await?;
243 let mut object = self
244 .shared_object_repo
245 .find_by_id(object_id)
246 .await?
247 .ok_or("Shared object not found".to_string())?;
248
249 if object.owner_id != owner.id {
251 return Err("Unauthorized: only owner can update object".to_string());
252 }
253
254 object.update(
256 dto.object_name,
257 dto.description,
258 dto.condition,
259 dto.is_available,
260 dto.rental_credits_per_day,
261 dto.deposit_credits,
262 dto.borrowing_duration_days,
263 dto.photos,
264 dto.location_details,
265 dto.usage_instructions,
266 )?;
267
268 let updated = self.shared_object_repo.update(&object).await?;
270
271 self.get_shared_object(updated.id).await
273 }
274
275 pub async fn mark_object_available(
280 &self,
281 object_id: Uuid,
282 user_id: Uuid,
283 organization_id: Uuid,
284 ) -> Result<SharedObjectResponseDto, String> {
285 let owner = self.resolve_owner(user_id, organization_id).await?;
286 let mut object = self
287 .shared_object_repo
288 .find_by_id(object_id)
289 .await?
290 .ok_or("Shared object not found".to_string())?;
291
292 if object.owner_id != owner.id {
294 return Err("Unauthorized: only owner can mark object as available".to_string());
295 }
296
297 object.mark_available()?;
299
300 let updated = self.shared_object_repo.update(&object).await?;
302
303 self.get_shared_object(updated.id).await
305 }
306
307 pub async fn mark_object_unavailable(
312 &self,
313 object_id: Uuid,
314 user_id: Uuid,
315 organization_id: Uuid,
316 ) -> Result<SharedObjectResponseDto, String> {
317 let owner = self.resolve_owner(user_id, organization_id).await?;
318 let mut object = self
319 .shared_object_repo
320 .find_by_id(object_id)
321 .await?
322 .ok_or("Shared object not found".to_string())?;
323
324 if object.owner_id != owner.id {
326 return Err("Unauthorized: only owner can mark object as unavailable".to_string());
327 }
328
329 object.mark_unavailable();
331
332 let updated = self.shared_object_repo.update(&object).await?;
334
335 self.get_shared_object(updated.id).await
337 }
338
339 pub async fn borrow_object(
349 &self,
350 object_id: Uuid,
351 user_id: Uuid,
352 organization_id: Uuid,
353 dto: BorrowObjectDto,
354 ) -> Result<SharedObjectResponseDto, String> {
355 let borrower = self.resolve_owner(user_id, organization_id).await?;
356 let borrower_id = borrower.id;
357 let mut object = self
358 .shared_object_repo
359 .find_by_id(object_id)
360 .await?
361 .ok_or("Shared object not found".to_string())?;
362
363 if let Some(deposit_amount) = object.deposit_credits {
365 if deposit_amount > 0 {
366 let mut borrower_balance = self
368 .credit_balance_repo
369 .get_or_create(borrower_id, object.building_id)
370 .await?;
371
372 let new_balance = borrower_balance.balance - deposit_amount;
375 if new_balance < -100 {
376 return Err(format!(
378 "Insufficient credit balance. Deposit required: {} credits. \
379 Your balance: {} credits. Trust limit: -100 credits.",
380 deposit_amount, borrower_balance.balance
381 ));
382 }
383
384 borrower_balance.spend_credits(deposit_amount)?;
386
387 self.credit_balance_repo.update(&borrower_balance).await?;
389 }
390 }
391
392 object.borrow(borrower_id, dto.duration_days)?;
394
395 let updated = self.shared_object_repo.update(&object).await?;
397
398 self.get_shared_object(updated.id).await
400 }
401
402 pub async fn return_object(
412 &self,
413 object_id: Uuid,
414 user_id: Uuid,
415 organization_id: Uuid,
416 ) -> Result<SharedObjectResponseDto, String> {
417 let returner = self.resolve_owner(user_id, organization_id).await?;
418 let returner_id = returner.id;
419 let mut object = self
420 .shared_object_repo
421 .find_by_id(object_id)
422 .await?
423 .ok_or("Shared object not found".to_string())?;
424
425 let (rental_cost, deposit) = object.calculate_total_cost();
427
428 if rental_cost > 0 || deposit > 0 {
429 let borrower_id = object
430 .current_borrower_id
431 .ok_or("No current borrower to refund".to_string())?;
432 let owner_id = object.owner_id;
433
434 let mut borrower_balance = self
436 .credit_balance_repo
437 .get_or_create(borrower_id, object.building_id)
438 .await?;
439
440 let mut owner_balance = self
442 .credit_balance_repo
443 .get_or_create(owner_id, object.building_id)
444 .await?;
445
446 if rental_cost > 0 {
448 borrower_balance.spend_credits(rental_cost)?;
449 owner_balance.earn_credits(rental_cost)?;
450 }
451
452 if deposit > 0 {
454 borrower_balance.earn_credits(deposit)?;
455 }
456
457 self.credit_balance_repo.update(&borrower_balance).await?;
459 self.credit_balance_repo.update(&owner_balance).await?;
460 }
461
462 object.return_object(returner_id)?;
464
465 let updated = self.shared_object_repo.update(&object).await?;
467
468 self.get_shared_object(updated.id).await
470 }
471
472 pub async fn delete_shared_object(
488 &self,
489 object_id: Uuid,
490 user_id: Uuid,
491 organization_id: Uuid,
492 actor_role: &str,
493 reason: Option<String>,
494 ) -> Result<(), String> {
495 let owner = self
496 .owner_repo
497 .find_by_user_id_and_organization(user_id, organization_id)
498 .await?;
499
500 let object = self
501 .shared_object_repo
502 .find_by_id(object_id)
503 .await?
504 .ok_or("Shared object not found".to_string())?;
505
506 let is_owner = owner
507 .as_ref()
508 .map(|o| object.owner_id == o.id)
509 .unwrap_or(false);
510
511 if !is_owner {
512 if crate::application::community_permissions::peut_moderer(actor_role) {
513 reason.filter(|r| !r.trim().is_empty()).ok_or_else(|| {
514 crate::application::error::MOTIF_MODERATION_REQUIS.to_string()
515 })?;
516 } else if owner.is_none() {
517 return Err(
518 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES.to_string()
519 );
520 } else {
521 return Err("Unauthorized: only owner can delete object".to_string());
522 }
523 }
524
525 if object.is_borrowed() {
527 return Err("Cannot delete object while it is borrowed".to_string());
528 }
529
530 self.shared_object_repo.delete(object_id).await?;
532
533 Ok(())
534 }
535
536 pub async fn get_object_statistics(
538 &self,
539 building_id: Uuid,
540 ) -> Result<SharedObjectStatisticsDto, String> {
541 let total_objects = self
542 .shared_object_repo
543 .count_by_building(building_id)
544 .await?;
545 let available_objects = self
546 .shared_object_repo
547 .count_available_by_building(building_id)
548 .await?;
549 let borrowed_objects = self
550 .shared_object_repo
551 .count_borrowed_by_building(building_id)
552 .await?;
553 let overdue_objects = self
554 .shared_object_repo
555 .count_overdue_by_building(building_id)
556 .await?;
557
558 let objects = self
560 .shared_object_repo
561 .find_by_building(building_id)
562 .await?;
563 let free_objects = objects.iter().filter(|o| o.is_free()).count() as i64;
564 let paid_objects = total_objects - free_objects;
565
566 let mut objects_by_category = Vec::new();
568 for category in [
569 SharedObjectCategory::Tools,
570 SharedObjectCategory::Books,
571 SharedObjectCategory::Electronics,
572 SharedObjectCategory::Sports,
573 SharedObjectCategory::Gardening,
574 SharedObjectCategory::Kitchen,
575 SharedObjectCategory::Baby,
576 SharedObjectCategory::Other,
577 ] {
578 let count = self
579 .shared_object_repo
580 .count_by_category(building_id, category.clone())
581 .await?;
582 if count > 0 {
583 objects_by_category.push(CategoryObjectCount { category, count });
584 }
585 }
586
587 Ok(SharedObjectStatisticsDto {
588 total_objects,
589 available_objects,
590 borrowed_objects,
591 overdue_objects,
592 free_objects,
593 paid_objects,
594 objects_by_category,
595 })
596 }
597
598 async fn enrich_objects_summary(
600 &self,
601 objects: Vec<SharedObject>,
602 ) -> Result<Vec<SharedObjectSummaryDto>, String> {
603 let mut enriched = Vec::new();
604
605 for object in objects {
606 let owner = self.owner_repo.find_by_id(object.owner_id).await?;
608 let owner_name = if let Some(owner) = owner {
609 format!("{} {}", owner.first_name, owner.last_name)
610 } else {
611 "Unknown Owner".to_string()
612 };
613
614 enriched.push(SharedObjectSummaryDto::from_shared_object(
615 object, owner_name,
616 ));
617 }
618
619 Ok(enriched)
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::application::dto::{OwnerFilters, PageRequest};
627 use crate::application::ports::{
628 OwnerCreditBalanceRepository, OwnerRepository, SharedObjectRepository,
629 };
630 use crate::domain::entities::{
631 ObjectCondition, Owner, OwnerCreditBalance, SharedObject, SharedObjectCategory,
632 };
633 use async_trait::async_trait;
634 use std::collections::HashMap;
635 use std::sync::{Arc, Mutex};
636 use uuid::Uuid;
637
638 struct MockSharedObjectRepo {
640 objects: Mutex<HashMap<Uuid, SharedObject>>,
641 }
642
643 impl MockSharedObjectRepo {
644 fn new() -> Self {
645 Self {
646 objects: Mutex::new(HashMap::new()),
647 }
648 }
649 }
650
651 #[async_trait]
652 impl SharedObjectRepository for MockSharedObjectRepo {
653 async fn create(&self, object: &SharedObject) -> Result<SharedObject, String> {
654 let mut map = self.objects.lock().unwrap();
655 map.insert(object.id, object.clone());
656 Ok(object.clone())
657 }
658
659 async fn find_by_id(&self, id: Uuid) -> Result<Option<SharedObject>, String> {
660 let map = self.objects.lock().unwrap();
661 Ok(map.get(&id).cloned())
662 }
663
664 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<SharedObject>, String> {
665 let map = self.objects.lock().unwrap();
666 Ok(map
667 .values()
668 .filter(|o| o.building_id == building_id)
669 .cloned()
670 .collect())
671 }
672
673 async fn find_available_by_building(
674 &self,
675 building_id: Uuid,
676 ) -> Result<Vec<SharedObject>, String> {
677 let map = self.objects.lock().unwrap();
678 Ok(map
679 .values()
680 .filter(|o| o.building_id == building_id && o.is_available)
681 .cloned()
682 .collect())
683 }
684
685 async fn find_borrowed_by_building(
686 &self,
687 building_id: Uuid,
688 ) -> Result<Vec<SharedObject>, String> {
689 let map = self.objects.lock().unwrap();
690 Ok(map
691 .values()
692 .filter(|o| o.building_id == building_id && o.is_borrowed())
693 .cloned()
694 .collect())
695 }
696
697 async fn find_overdue_by_building(
698 &self,
699 building_id: Uuid,
700 ) -> Result<Vec<SharedObject>, String> {
701 let map = self.objects.lock().unwrap();
702 Ok(map
703 .values()
704 .filter(|o| o.building_id == building_id && o.is_overdue())
705 .cloned()
706 .collect())
707 }
708
709 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<SharedObject>, String> {
710 let map = self.objects.lock().unwrap();
711 Ok(map
712 .values()
713 .filter(|o| o.owner_id == owner_id)
714 .cloned()
715 .collect())
716 }
717
718 async fn find_borrowed_by_user(
719 &self,
720 borrower_id: Uuid,
721 ) -> Result<Vec<SharedObject>, String> {
722 let map = self.objects.lock().unwrap();
723 Ok(map
724 .values()
725 .filter(|o| o.current_borrower_id == Some(borrower_id))
726 .cloned()
727 .collect())
728 }
729
730 async fn find_by_category(
731 &self,
732 building_id: Uuid,
733 category: SharedObjectCategory,
734 ) -> Result<Vec<SharedObject>, String> {
735 let map = self.objects.lock().unwrap();
736 Ok(map
737 .values()
738 .filter(|o| o.building_id == building_id && o.object_category == category)
739 .cloned()
740 .collect())
741 }
742
743 async fn find_free_by_building(
744 &self,
745 building_id: Uuid,
746 ) -> Result<Vec<SharedObject>, String> {
747 let map = self.objects.lock().unwrap();
748 Ok(map
749 .values()
750 .filter(|o| o.building_id == building_id && o.is_free())
751 .cloned()
752 .collect())
753 }
754
755 async fn update(&self, object: &SharedObject) -> Result<SharedObject, String> {
756 let mut map = self.objects.lock().unwrap();
757 map.insert(object.id, object.clone());
758 Ok(object.clone())
759 }
760
761 async fn delete(&self, id: Uuid) -> Result<(), String> {
762 let mut map = self.objects.lock().unwrap();
763 map.remove(&id);
764 Ok(())
765 }
766
767 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
768 let map = self.objects.lock().unwrap();
769 Ok(map
770 .values()
771 .filter(|o| o.building_id == building_id)
772 .count() as i64)
773 }
774
775 async fn count_available_by_building(&self, building_id: Uuid) -> Result<i64, String> {
776 let map = self.objects.lock().unwrap();
777 Ok(map
778 .values()
779 .filter(|o| o.building_id == building_id && o.is_available)
780 .count() as i64)
781 }
782
783 async fn count_borrowed_by_building(&self, building_id: Uuid) -> Result<i64, String> {
784 let map = self.objects.lock().unwrap();
785 Ok(map
786 .values()
787 .filter(|o| o.building_id == building_id && o.is_borrowed())
788 .count() as i64)
789 }
790
791 async fn count_overdue_by_building(&self, building_id: Uuid) -> Result<i64, String> {
792 let map = self.objects.lock().unwrap();
793 Ok(map
794 .values()
795 .filter(|o| o.building_id == building_id && o.is_overdue())
796 .count() as i64)
797 }
798
799 async fn count_by_category(
800 &self,
801 building_id: Uuid,
802 category: SharedObjectCategory,
803 ) -> Result<i64, String> {
804 let map = self.objects.lock().unwrap();
805 Ok(map
806 .values()
807 .filter(|o| o.building_id == building_id && o.object_category == category)
808 .count() as i64)
809 }
810 }
811
812 struct MockOwnerRepo {
814 owners: Mutex<HashMap<Uuid, Owner>>,
815 }
816
817 impl MockOwnerRepo {
818 fn new() -> Self {
819 Self {
820 owners: Mutex::new(HashMap::new()),
821 }
822 }
823 fn add_owner(&self, owner: Owner) {
824 self.owners.lock().unwrap().insert(owner.id, owner);
825 }
826 }
827
828 #[async_trait]
829 impl OwnerRepository for MockOwnerRepo {
830 async fn create(&self, owner: &Owner) -> Result<Owner, String> {
831 self.owners.lock().unwrap().insert(owner.id, owner.clone());
832 Ok(owner.clone())
833 }
834 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
835 Ok(self.owners.lock().unwrap().get(&id).cloned())
836 }
837 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
838 Ok(self
839 .owners
840 .lock()
841 .unwrap()
842 .values()
843 .find(|o| o.user_id == Some(user_id))
844 .cloned())
845 }
846 async fn find_by_user_id_and_organization(
847 &self,
848 user_id: Uuid,
849 org_id: Uuid,
850 ) -> Result<Option<Owner>, String> {
851 Ok(self
852 .owners
853 .lock()
854 .unwrap()
855 .values()
856 .find(|o| o.user_id == Some(user_id) && o.organization_id == org_id)
857 .cloned())
858 }
859 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
860 Ok(self
861 .owners
862 .lock()
863 .unwrap()
864 .values()
865 .find(|o| o.email == email)
866 .cloned())
867 }
868 async fn find_all(&self) -> Result<Vec<Owner>, String> {
869 Ok(self.owners.lock().unwrap().values().cloned().collect())
870 }
871 async fn find_all_paginated(
872 &self,
873 _p: &PageRequest,
874 _f: &OwnerFilters,
875 ) -> Result<(Vec<Owner>, i64), String> {
876 let all: Vec<_> = self.owners.lock().unwrap().values().cloned().collect();
877 let c = all.len() as i64;
878 Ok((all, c))
879 }
880 async fn update(&self, owner: &Owner) -> Result<Owner, String> {
881 self.owners.lock().unwrap().insert(owner.id, owner.clone());
882 Ok(owner.clone())
883 }
884 async fn delete(&self, id: Uuid) -> Result<bool, String> {
885 Ok(self.owners.lock().unwrap().remove(&id).is_some())
886 }
887 async fn set_user_link(
888 &self,
889 owner_id: Uuid,
890 user_id: Option<Uuid>,
891 ) -> Result<bool, String> {
892 let mut map = self.owners.lock().unwrap();
893 if let Some(o) = map.get_mut(&owner_id) {
894 o.user_id = user_id;
895 Ok(true)
896 } else {
897 Ok(false)
898 }
899 }
900 }
901
902 struct MockCreditBalanceRepo {
904 balances: Mutex<HashMap<(Uuid, Uuid), OwnerCreditBalance>>,
905 }
906
907 impl MockCreditBalanceRepo {
908 fn new() -> Self {
909 Self {
910 balances: Mutex::new(HashMap::new()),
911 }
912 }
913 }
914
915 #[async_trait]
916 impl OwnerCreditBalanceRepository for MockCreditBalanceRepo {
917 async fn create(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
918 let mut map = self.balances.lock().unwrap();
919 map.insert((balance.owner_id, balance.building_id), balance.clone());
920 Ok(balance.clone())
921 }
922 async fn find_by_owner_and_building(
923 &self,
924 owner_id: Uuid,
925 building_id: Uuid,
926 ) -> Result<Option<OwnerCreditBalance>, String> {
927 Ok(self
928 .balances
929 .lock()
930 .unwrap()
931 .get(&(owner_id, building_id))
932 .cloned())
933 }
934 async fn find_by_building(
935 &self,
936 building_id: Uuid,
937 ) -> Result<Vec<OwnerCreditBalance>, String> {
938 Ok(self
939 .balances
940 .lock()
941 .unwrap()
942 .values()
943 .filter(|b| b.building_id == building_id)
944 .cloned()
945 .collect())
946 }
947 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerCreditBalance>, String> {
948 Ok(self
949 .balances
950 .lock()
951 .unwrap()
952 .values()
953 .filter(|b| b.owner_id == owner_id)
954 .cloned()
955 .collect())
956 }
957 async fn get_or_create(
958 &self,
959 owner_id: Uuid,
960 building_id: Uuid,
961 ) -> Result<OwnerCreditBalance, String> {
962 let mut map = self.balances.lock().unwrap();
963 let key = (owner_id, building_id);
964 if let Some(existing) = map.get(&key) {
965 Ok(existing.clone())
966 } else {
967 let balance = OwnerCreditBalance::new(owner_id, building_id);
968 map.insert(key, balance.clone());
969 Ok(balance)
970 }
971 }
972 async fn update(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
973 let mut map = self.balances.lock().unwrap();
974 map.insert((balance.owner_id, balance.building_id), balance.clone());
975 Ok(balance.clone())
976 }
977 async fn delete(&self, owner_id: Uuid, building_id: Uuid) -> Result<bool, String> {
978 Ok(self
979 .balances
980 .lock()
981 .unwrap()
982 .remove(&(owner_id, building_id))
983 .is_some())
984 }
985 async fn get_leaderboard(
986 &self,
987 _building_id: Uuid,
988 _limit: i32,
989 ) -> Result<Vec<OwnerCreditBalance>, String> {
990 Ok(vec![])
991 }
992 async fn count_active_participants(&self, _building_id: Uuid) -> Result<i64, String> {
993 Ok(0)
994 }
995 async fn get_total_credits_in_circulation(
996 &self,
997 _building_id: Uuid,
998 ) -> Result<i32, String> {
999 Ok(0)
1000 }
1001 }
1002
1003 fn create_test_owner(user_id: Uuid, org_id: Uuid) -> Owner {
1005 let mut owner = Owner::new(
1006 org_id,
1007 "Jean".to_string(),
1008 "Dupont".to_string(),
1009 "jean@test.com".to_string(),
1010 None,
1011 "Rue Test".to_string(),
1012 "Brussels".to_string(),
1013 "1000".to_string(),
1014 "Belgium".to_string(),
1015 )
1016 .unwrap();
1017 owner.user_id = Some(user_id);
1018 owner
1019 }
1020
1021 fn setup() -> (SharedObjectUseCases, Uuid, Uuid, Uuid, Uuid) {
1022 let user_id = Uuid::new_v4();
1023 let org_id = Uuid::new_v4();
1024 let building_id = Uuid::new_v4();
1025
1026 let obj_repo = Arc::new(MockSharedObjectRepo::new());
1027 let owner_repo = Arc::new(MockOwnerRepo::new());
1028 let credit_repo = Arc::new(MockCreditBalanceRepo::new());
1029
1030 let owner = create_test_owner(user_id, org_id);
1031 let owner_id = owner.id;
1032 owner_repo.add_owner(owner);
1033
1034 let uc = SharedObjectUseCases::new(
1035 obj_repo as Arc<dyn SharedObjectRepository>,
1036 owner_repo as Arc<dyn OwnerRepository>,
1037 credit_repo as Arc<dyn OwnerCreditBalanceRepository>,
1038 );
1039
1040 (uc, user_id, org_id, building_id, owner_id)
1041 }
1042
1043 fn make_create_dto(building_id: Uuid) -> CreateSharedObjectDto {
1044 CreateSharedObjectDto {
1045 building_id,
1046 object_category: SharedObjectCategory::Tools,
1047 object_name: "Power Drill".to_string(),
1048 description: "18V cordless drill with battery".to_string(),
1049 condition: ObjectCondition::Good,
1050 is_available: true,
1051 rental_credits_per_day: Some(2),
1052 deposit_credits: Some(10),
1053 borrowing_duration_days: Some(7),
1054 photos: None,
1055 location_details: Some("Basement".to_string()),
1056 usage_instructions: None,
1057 }
1058 }
1059
1060 #[tokio::test]
1063 async fn test_create_shared_object_success() {
1064 let (uc, user_id, org_id, building_id, _) = setup();
1065 let dto = make_create_dto(building_id);
1066 let result = uc.create_shared_object(user_id, org_id, dto).await;
1067 assert!(result.is_ok());
1068 let resp = result.unwrap();
1069 assert_eq!(resp.object_name, "Power Drill");
1070 assert_eq!(resp.owner_name, "Jean Dupont");
1071 }
1072
1073 #[tokio::test]
1074 async fn test_get_shared_object_success() {
1075 let (uc, user_id, org_id, building_id, _) = setup();
1076 let dto = make_create_dto(building_id);
1077 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1078
1079 let result = uc.get_shared_object(created.id).await;
1080 assert!(result.is_ok());
1081 assert_eq!(result.unwrap().id, created.id);
1082 }
1083
1084 #[tokio::test]
1085 async fn test_get_shared_object_not_found() {
1086 let (uc, _, _, _, _) = setup();
1087 let result = uc.get_shared_object(Uuid::new_v4()).await;
1088 assert!(result.is_err());
1089 assert_eq!(result.unwrap_err(), "Shared object not found");
1090 }
1091
1092 #[tokio::test]
1093 async fn test_delete_shared_object_success() {
1094 let (uc, user_id, org_id, building_id, _) = setup();
1095 let dto = make_create_dto(building_id);
1096 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1097
1098 let result = uc
1099 .delete_shared_object(created.id, user_id, org_id, "owner", None)
1100 .await;
1101 assert!(result.is_ok());
1102 }
1103
1104 #[tokio::test]
1105 async fn test_delete_shared_object_wrong_owner() {
1106 let (uc, user_id, org_id, building_id, _) = setup();
1107 let dto = make_create_dto(building_id);
1108 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1109
1110 let other = Uuid::new_v4();
1112 let result = uc
1113 .delete_shared_object(created.id, other, org_id, "owner", None)
1114 .await;
1115 assert!(result.is_err());
1116 assert_eq!(
1117 result.unwrap_err(),
1118 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES,
1119 "le refus doit être celui, nommé, opposé à qui n'est pas copropriétaire"
1120 );
1121 }
1122
1123 #[tokio::test]
1128 async fn happy_syndic_moderator_supprime_un_objet_litigieux_avec_motif() {
1129 let (uc, user_id, org_id, building_id, _) = setup();
1130 let dto = make_create_dto(building_id);
1131 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1132
1133 let syndic_id = Uuid::new_v4(); let result = uc
1135 .delete_shared_object(
1136 created.id,
1137 syndic_id,
1138 org_id,
1139 "syndic",
1140 Some("Objet dangereux signalé".to_string()),
1141 )
1142 .await;
1143 assert!(
1144 result.is_ok(),
1145 "un syndic modérateur doit pouvoir supprimer un objet litigieux : {:?}",
1146 result.err()
1147 );
1148 assert!(uc.get_shared_object(created.id).await.is_err());
1149 }
1150
1151 #[tokio::test]
1152 async fn negative_moderation_dun_objet_sans_motif_est_refusee() {
1153 let (uc, user_id, org_id, building_id, _) = setup();
1154 let dto = make_create_dto(building_id);
1155 let created = uc.create_shared_object(user_id, org_id, dto).await.unwrap();
1156
1157 let syndic_id = Uuid::new_v4();
1158 let result = uc
1159 .delete_shared_object(created.id, syndic_id, org_id, "syndic", None)
1160 .await;
1161 assert!(result.is_err());
1162 assert_eq!(
1163 result.unwrap_err(),
1164 crate::application::error::MOTIF_MODERATION_REQUIS
1165 );
1166 }
1167
1168 #[tokio::test]
1169 async fn edge_syndic_cumulant_le_role_owner_supprime_son_propre_objet_sans_motif() {
1170 let obj_repo = Arc::new(MockSharedObjectRepo::new());
1174 let owner_repo = Arc::new(MockOwnerRepo::new());
1175 let credit_repo = Arc::new(MockCreditBalanceRepo::new());
1176
1177 let syndic_owner_user_id = Uuid::new_v4();
1178 let org_id = Uuid::new_v4();
1179 let building_id = Uuid::new_v4();
1180 owner_repo.add_owner(create_test_owner(syndic_owner_user_id, org_id));
1181
1182 let uc = SharedObjectUseCases::new(
1183 obj_repo as Arc<dyn SharedObjectRepository>,
1184 owner_repo as Arc<dyn OwnerRepository>,
1185 credit_repo as Arc<dyn OwnerCreditBalanceRepository>,
1186 );
1187
1188 let dto = make_create_dto(building_id);
1189 let created = uc
1190 .create_shared_object(syndic_owner_user_id, org_id, dto)
1191 .await
1192 .unwrap();
1193
1194 let result = uc
1195 .delete_shared_object(created.id, syndic_owner_user_id, org_id, "syndic", None)
1196 .await;
1197 assert!(
1198 result.is_ok(),
1199 "le propriétaire de l'objet n'a pas à motiver sa propre suppression : {:?}",
1200 result.err()
1201 );
1202 }
1203
1204 #[tokio::test]
1205 async fn test_list_building_objects() {
1206 let (uc, user_id, org_id, building_id, _) = setup();
1207
1208 let dto1 = make_create_dto(building_id);
1209 let mut dto2 = make_create_dto(building_id);
1210 dto2.object_name = "Hammer".to_string();
1211 dto2.description = "Steel hammer for nails".to_string();
1212
1213 uc.create_shared_object(user_id, org_id, dto1)
1214 .await
1215 .unwrap();
1216 uc.create_shared_object(user_id, org_id, dto2)
1217 .await
1218 .unwrap();
1219
1220 let result = uc.list_building_objects(building_id).await;
1221 assert!(result.is_ok());
1222 assert_eq!(result.unwrap().len(), 2);
1223 }
1224
1225 #[tokio::test]
1226 async fn test_owner_not_found() {
1227 let obj_repo = Arc::new(MockSharedObjectRepo::new());
1228 let owner_repo = Arc::new(MockOwnerRepo::new());
1229 let credit_repo = Arc::new(MockCreditBalanceRepo::new());
1230 let uc = SharedObjectUseCases::new(
1233 obj_repo as Arc<dyn SharedObjectRepository>,
1234 owner_repo as Arc<dyn OwnerRepository>,
1235 credit_repo as Arc<dyn OwnerCreditBalanceRepository>,
1236 );
1237
1238 let dto = make_create_dto(Uuid::new_v4());
1239 let result = uc
1240 .create_shared_object(Uuid::new_v4(), Uuid::new_v4(), dto)
1241 .await;
1242 assert!(result.is_err());
1243 assert_eq!(
1244 result.unwrap_err(),
1245 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES,
1246 "le refus doit être celui, nommé, opposé à qui n'est pas copropriétaire"
1247 );
1248 }
1249}