koprogo_api/application/use_cases/
shared_object_use_cases.rs

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    /// Resolve user_id to owner via organization lookup
32    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    /// Create a new shared object
44    ///
45    /// # Authorization
46    /// - Must be an owner in the organization (owners lend to other owners/tenants)
47    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    /// Get shared object by ID with owner/borrower name enrichment
81    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        // Enrich with borrower name if borrowed
99        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    /// List all shared objects for a building
114    ///
115    /// # Returns
116    /// - Objects sorted by available (DESC), object_name (ASC)
117    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    /// List available shared objects for a building (marketplace view)
129    ///
130    /// # Returns
131    /// - Only available objects (is_available = true)
132    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    /// List borrowed shared objects for a building
144    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    /// List overdue shared objects for a building
156    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    /// List all shared objects created by an owner
168    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    /// List all shared objects currently borrowed by a user
177    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    /// List shared objects by category (Tools, Books, Electronics, etc.)
191    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    /// List free/volunteer shared objects for a building
204    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    /// Update a shared object
216    ///
217    /// # Authorization
218    /// - Only owner can update their object
219    /// - Cannot update if currently borrowed
220    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        // Authorization: only owner can update
235        if object.owner_id != owner.id {
236            return Err("Unauthorized: only owner can update object".to_string());
237        }
238
239        // Update object (domain validates business rules including borrowed check)
240        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        // Persist changes
254        let updated = self.shared_object_repo.update(&object).await?;
255
256        // Return enriched response
257        self.get_shared_object(updated.id).await
258    }
259
260    /// Mark shared object as available
261    ///
262    /// # Authorization
263    /// - Only owner can mark their object as available
264    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        // Authorization: only owner can mark available
278        if object.owner_id != owner.id {
279            return Err("Unauthorized: only owner can mark object as available".to_string());
280        }
281
282        // Mark available (checks not borrowed)
283        object.mark_available()?;
284
285        // Persist changes
286        let updated = self.shared_object_repo.update(&object).await?;
287
288        // Return enriched response
289        self.get_shared_object(updated.id).await
290    }
291
292    /// Mark shared object as unavailable
293    ///
294    /// # Authorization
295    /// - Only owner can mark their object as unavailable
296    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        // Authorization: only owner can mark unavailable
310        if object.owner_id != owner.id {
311            return Err("Unauthorized: only owner can mark object as unavailable".to_string());
312        }
313
314        // Mark unavailable
315        object.mark_unavailable();
316
317        // Persist changes
318        let updated = self.shared_object_repo.update(&object).await?;
319
320        // Return enriched response
321        self.get_shared_object(updated.id).await
322    }
323
324    /// Borrow a shared object
325    ///
326    /// # Authorization
327    /// - Borrower must not be the owner
328    /// - Object must be available
329    ///
330    /// # SEL Integration
331    /// - For paid objects, holds deposit from borrower's credit balance
332    /// - Rental fee calculated on return based on actual days borrowed
333    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        // SEL Integration: Hold deposit for paid objects
349        if let Some(deposit_amount) = object.deposit_credits {
350            if deposit_amount > 0 {
351                // Get or create borrower's credit balance
352                let mut borrower_balance = self
353                    .credit_balance_repo
354                    .get_or_create(borrower_id, object.building_id)
355                    .await?;
356
357                // Check if borrower has sufficient balance (allowing negative for trust-based)
358                // But we still validate to warn if balance goes too negative
359                let new_balance = borrower_balance.balance - deposit_amount;
360                if new_balance < -100 {
361                    // Trust limit: -100 credits
362                    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                // Hold deposit (spend from borrower's balance)
370                borrower_balance.spend_credits(deposit_amount)?;
371
372                // Persist balance change
373                self.credit_balance_repo.update(&borrower_balance).await?;
374            }
375        }
376
377        // Borrow object (validates business rules: owner != borrower, is_available, etc.)
378        object.borrow(borrower_id, dto.duration_days)?;
379
380        // Persist changes
381        let updated = self.shared_object_repo.update(&object).await?;
382
383        // Return enriched response
384        self.get_shared_object(updated.id).await
385    }
386
387    /// Return a borrowed object
388    ///
389    /// # Authorization
390    /// - Only borrower can return object
391    ///
392    /// # SEL Integration
393    /// - For paid objects, calculates rental fee based on actual days
394    /// - Transfers rental credits from borrower to owner
395    /// - Refunds deposit to borrower
396    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        // SEL Integration: Calculate and transfer credits for paid objects
411        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            // Get or create borrower's credit balance
420            let mut borrower_balance = self
421                .credit_balance_repo
422                .get_or_create(borrower_id, object.building_id)
423                .await?;
424
425            // Get or create owner's credit balance
426            let mut owner_balance = self
427                .credit_balance_repo
428                .get_or_create(owner_id, object.building_id)
429                .await?;
430
431            // Transfer rental cost from borrower to owner
432            if rental_cost > 0 {
433                borrower_balance.spend_credits(rental_cost)?;
434                owner_balance.earn_credits(rental_cost)?;
435            }
436
437            // Refund deposit to borrower
438            if deposit > 0 {
439                borrower_balance.earn_credits(deposit)?;
440            }
441
442            // Persist balance changes
443            self.credit_balance_repo.update(&borrower_balance).await?;
444            self.credit_balance_repo.update(&owner_balance).await?;
445        }
446
447        // Return object (validates only borrower can return)
448        object.return_object(returner_id)?;
449
450        // Persist changes
451        let updated = self.shared_object_repo.update(&object).await?;
452
453        // Return enriched response
454        self.get_shared_object(updated.id).await
455    }
456
457    /// Delete a shared object
458    ///
459    /// # Authorization
460    /// - Only owner can delete their object
461    /// - Cannot delete if currently borrowed
462    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        // Authorization: only owner can delete
476        if object.owner_id != owner.id {
477            return Err("Unauthorized: only owner can delete object".to_string());
478        }
479
480        // Business rule: cannot delete if borrowed
481        if object.is_borrowed() {
482            return Err("Cannot delete object while it is borrowed".to_string());
483        }
484
485        // Delete object
486        self.shared_object_repo.delete(object_id).await?;
487
488        Ok(())
489    }
490
491    /// Get shared object statistics for a building
492    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        // Calculate free/paid objects
514        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        // Count by category
522        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    /// Helper method to enrich objects with owner names
554    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            // Get owner name
562            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    // ── Mock SharedObjectRepository ─────────────────────────────────────────
594    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    // ── Mock OwnerRepository ────────────────────────────────────────────────
768    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    // ── Mock OwnerCreditBalanceRepository ────────────────────────────────────
845    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    // ── Helpers ─────────────────────────────────────────────────────────────
946    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    // ── Tests ───────────────────────────────────────────────────────────────
1003
1004    #[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        // Another user tries to delete -- will fail because user not found as owner
1051        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        // No owner added
1084
1085        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}