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    /// Create a new shared object
32    ///
33    /// # Authorization
34    /// - Owner must exist in the system
35    pub async fn create_shared_object(
36        &self,
37        owner_id: Uuid,
38        dto: CreateSharedObjectDto,
39    ) -> Result<SharedObjectResponseDto, String> {
40        // Verify owner exists
41        let owner = self
42            .owner_repo
43            .find_by_id(owner_id)
44            .await?
45            .ok_or("Owner not found".to_string())?;
46
47        // Create shared object entity (validates business rules)
48        let object = SharedObject::new(
49            owner_id,
50            dto.building_id,
51            dto.object_category,
52            dto.object_name,
53            dto.description,
54            dto.condition,
55            dto.is_available,
56            dto.rental_credits_per_day,
57            dto.deposit_credits,
58            dto.borrowing_duration_days,
59            dto.photos,
60            dto.location_details,
61            dto.usage_instructions,
62        )?;
63
64        // Persist object
65        let created = self.shared_object_repo.create(&object).await?;
66
67        // Return enriched response
68        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
69        Ok(SharedObjectResponseDto::from_shared_object(
70            created, owner_name, None,
71        ))
72    }
73
74    /// Get shared object by ID with owner/borrower name enrichment
75    pub async fn get_shared_object(
76        &self,
77        object_id: Uuid,
78    ) -> Result<SharedObjectResponseDto, String> {
79        let object = self
80            .shared_object_repo
81            .find_by_id(object_id)
82            .await?
83            .ok_or("Shared object not found".to_string())?;
84
85        // Enrich with owner name
86        let owner = self
87            .owner_repo
88            .find_by_id(object.owner_id)
89            .await?
90            .ok_or("Owner not found".to_string())?;
91        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
92
93        // Enrich with borrower name if borrowed
94        let borrower_name = if let Some(borrower_id) = object.current_borrower_id {
95            let borrower = self.owner_repo.find_by_id(borrower_id).await?;
96            borrower.map(|b| format!("{} {}", b.first_name, b.last_name))
97        } else {
98            None
99        };
100
101        Ok(SharedObjectResponseDto::from_shared_object(
102            object,
103            owner_name,
104            borrower_name,
105        ))
106    }
107
108    /// List all shared objects for a building
109    ///
110    /// # Returns
111    /// - Objects sorted by available (DESC), object_name (ASC)
112    pub async fn list_building_objects(
113        &self,
114        building_id: Uuid,
115    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
116        let objects = self
117            .shared_object_repo
118            .find_by_building(building_id)
119            .await?;
120        self.enrich_objects_summary(objects).await
121    }
122
123    /// List available shared objects for a building (marketplace view)
124    ///
125    /// # Returns
126    /// - Only available objects (is_available = true)
127    pub async fn list_available_objects(
128        &self,
129        building_id: Uuid,
130    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
131        let objects = self
132            .shared_object_repo
133            .find_available_by_building(building_id)
134            .await?;
135        self.enrich_objects_summary(objects).await
136    }
137
138    /// List borrowed shared objects for a building
139    pub async fn list_borrowed_objects(
140        &self,
141        building_id: Uuid,
142    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
143        let objects = self
144            .shared_object_repo
145            .find_borrowed_by_building(building_id)
146            .await?;
147        self.enrich_objects_summary(objects).await
148    }
149
150    /// List overdue shared objects for a building
151    pub async fn list_overdue_objects(
152        &self,
153        building_id: Uuid,
154    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
155        let objects = self
156            .shared_object_repo
157            .find_overdue_by_building(building_id)
158            .await?;
159        self.enrich_objects_summary(objects).await
160    }
161
162    /// List all shared objects created by an owner
163    pub async fn list_owner_objects(
164        &self,
165        owner_id: Uuid,
166    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
167        let objects = self.shared_object_repo.find_by_owner(owner_id).await?;
168        self.enrich_objects_summary(objects).await
169    }
170
171    /// List all shared objects currently borrowed by a user
172    pub async fn list_user_borrowed_objects(
173        &self,
174        borrower_id: Uuid,
175    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
176        let objects = self
177            .shared_object_repo
178            .find_borrowed_by_user(borrower_id)
179            .await?;
180        self.enrich_objects_summary(objects).await
181    }
182
183    /// List shared objects by category (Tools, Books, Electronics, etc.)
184    pub async fn list_objects_by_category(
185        &self,
186        building_id: Uuid,
187        category: SharedObjectCategory,
188    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
189        let objects = self
190            .shared_object_repo
191            .find_by_category(building_id, category)
192            .await?;
193        self.enrich_objects_summary(objects).await
194    }
195
196    /// List free/volunteer shared objects for a building
197    pub async fn list_free_objects(
198        &self,
199        building_id: Uuid,
200    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
201        let objects = self
202            .shared_object_repo
203            .find_free_by_building(building_id)
204            .await?;
205        self.enrich_objects_summary(objects).await
206    }
207
208    /// Update a shared object
209    ///
210    /// # Authorization
211    /// - Only owner can update their object
212    /// - Cannot update if currently borrowed
213    pub async fn update_shared_object(
214        &self,
215        object_id: Uuid,
216        actor_id: Uuid,
217        dto: UpdateSharedObjectDto,
218    ) -> Result<SharedObjectResponseDto, String> {
219        let mut object = self
220            .shared_object_repo
221            .find_by_id(object_id)
222            .await?
223            .ok_or("Shared object not found".to_string())?;
224
225        // Authorization: only owner can update
226        if object.owner_id != actor_id {
227            return Err("Unauthorized: only owner can update object".to_string());
228        }
229
230        // Update object (domain validates business rules including borrowed check)
231        object.update(
232            dto.object_name,
233            dto.description,
234            dto.condition,
235            dto.is_available,
236            dto.rental_credits_per_day,
237            dto.deposit_credits,
238            dto.borrowing_duration_days,
239            dto.photos,
240            dto.location_details,
241            dto.usage_instructions,
242        )?;
243
244        // Persist changes
245        let updated = self.shared_object_repo.update(&object).await?;
246
247        // Return enriched response
248        self.get_shared_object(updated.id).await
249    }
250
251    /// Mark shared object as available
252    ///
253    /// # Authorization
254    /// - Only owner can mark their object as available
255    pub async fn mark_object_available(
256        &self,
257        object_id: Uuid,
258        actor_id: Uuid,
259    ) -> Result<SharedObjectResponseDto, String> {
260        let mut object = self
261            .shared_object_repo
262            .find_by_id(object_id)
263            .await?
264            .ok_or("Shared object not found".to_string())?;
265
266        // Authorization: only owner can mark available
267        if object.owner_id != actor_id {
268            return Err("Unauthorized: only owner can mark object as available".to_string());
269        }
270
271        // Mark available (checks not borrowed)
272        object.mark_available()?;
273
274        // Persist changes
275        let updated = self.shared_object_repo.update(&object).await?;
276
277        // Return enriched response
278        self.get_shared_object(updated.id).await
279    }
280
281    /// Mark shared object as unavailable
282    ///
283    /// # Authorization
284    /// - Only owner can mark their object as unavailable
285    pub async fn mark_object_unavailable(
286        &self,
287        object_id: Uuid,
288        actor_id: Uuid,
289    ) -> Result<SharedObjectResponseDto, String> {
290        let mut object = self
291            .shared_object_repo
292            .find_by_id(object_id)
293            .await?
294            .ok_or("Shared object not found".to_string())?;
295
296        // Authorization: only owner can mark unavailable
297        if object.owner_id != actor_id {
298            return Err("Unauthorized: only owner can mark object as unavailable".to_string());
299        }
300
301        // Mark unavailable
302        object.mark_unavailable();
303
304        // Persist changes
305        let updated = self.shared_object_repo.update(&object).await?;
306
307        // Return enriched response
308        self.get_shared_object(updated.id).await
309    }
310
311    /// Borrow a shared object
312    ///
313    /// # Authorization
314    /// - Borrower must not be the owner
315    /// - Object must be available
316    ///
317    /// # SEL Integration
318    /// - For paid objects, holds deposit from borrower's credit balance
319    /// - Rental fee calculated on return based on actual days borrowed
320    pub async fn borrow_object(
321        &self,
322        object_id: Uuid,
323        borrower_id: Uuid,
324        dto: BorrowObjectDto,
325    ) -> Result<SharedObjectResponseDto, String> {
326        let mut object = self
327            .shared_object_repo
328            .find_by_id(object_id)
329            .await?
330            .ok_or("Shared object not found".to_string())?;
331
332        // SEL Integration: Hold deposit for paid objects
333        if let Some(deposit_amount) = object.deposit_credits {
334            if deposit_amount > 0 {
335                // Get or create borrower's credit balance
336                let mut borrower_balance = self
337                    .credit_balance_repo
338                    .get_or_create(borrower_id, object.building_id)
339                    .await?;
340
341                // Check if borrower has sufficient balance (allowing negative for trust-based)
342                // But we still validate to warn if balance goes too negative
343                let new_balance = borrower_balance.balance - deposit_amount;
344                if new_balance < -100 {
345                    // Trust limit: -100 credits
346                    return Err(format!(
347                        "Insufficient credit balance. Deposit required: {} credits. \
348                        Your balance: {} credits. Trust limit: -100 credits.",
349                        deposit_amount, borrower_balance.balance
350                    ));
351                }
352
353                // Hold deposit (spend from borrower's balance)
354                borrower_balance.spend_credits(deposit_amount)?;
355
356                // Persist balance change
357                self.credit_balance_repo.update(&borrower_balance).await?;
358            }
359        }
360
361        // Borrow object (validates business rules: owner != borrower, is_available, etc.)
362        object.borrow(borrower_id, dto.duration_days)?;
363
364        // Persist changes
365        let updated = self.shared_object_repo.update(&object).await?;
366
367        // Return enriched response
368        self.get_shared_object(updated.id).await
369    }
370
371    /// Return a borrowed object
372    ///
373    /// # Authorization
374    /// - Only borrower can return object
375    ///
376    /// # SEL Integration
377    /// - For paid objects, calculates rental fee based on actual days
378    /// - Transfers rental credits from borrower to owner
379    /// - Refunds deposit to borrower
380    pub async fn return_object(
381        &self,
382        object_id: Uuid,
383        returner_id: Uuid,
384    ) -> Result<SharedObjectResponseDto, String> {
385        let mut object = self
386            .shared_object_repo
387            .find_by_id(object_id)
388            .await?
389            .ok_or("Shared object not found".to_string())?;
390
391        // SEL Integration: Calculate and transfer credits for paid objects
392        let (rental_cost, deposit) = object.calculate_total_cost();
393
394        if rental_cost > 0 || deposit > 0 {
395            let borrower_id = object
396                .current_borrower_id
397                .ok_or("No current borrower to refund".to_string())?;
398            let owner_id = object.owner_id;
399
400            // Get or create borrower's credit balance
401            let mut borrower_balance = self
402                .credit_balance_repo
403                .get_or_create(borrower_id, object.building_id)
404                .await?;
405
406            // Get or create owner's credit balance
407            let mut owner_balance = self
408                .credit_balance_repo
409                .get_or_create(owner_id, object.building_id)
410                .await?;
411
412            // Transfer rental cost from borrower to owner
413            if rental_cost > 0 {
414                borrower_balance.spend_credits(rental_cost)?;
415                owner_balance.earn_credits(rental_cost)?;
416            }
417
418            // Refund deposit to borrower
419            if deposit > 0 {
420                borrower_balance.earn_credits(deposit)?;
421            }
422
423            // Persist balance changes
424            self.credit_balance_repo.update(&borrower_balance).await?;
425            self.credit_balance_repo.update(&owner_balance).await?;
426        }
427
428        // Return object (validates only borrower can return)
429        object.return_object(returner_id)?;
430
431        // Persist changes
432        let updated = self.shared_object_repo.update(&object).await?;
433
434        // Return enriched response
435        self.get_shared_object(updated.id).await
436    }
437
438    /// Delete a shared object
439    ///
440    /// # Authorization
441    /// - Only owner can delete their object
442    /// - Cannot delete if currently borrowed
443    pub async fn delete_shared_object(
444        &self,
445        object_id: Uuid,
446        actor_id: Uuid,
447    ) -> Result<(), String> {
448        let object = self
449            .shared_object_repo
450            .find_by_id(object_id)
451            .await?
452            .ok_or("Shared object not found".to_string())?;
453
454        // Authorization: only owner can delete
455        if object.owner_id != actor_id {
456            return Err("Unauthorized: only owner can delete object".to_string());
457        }
458
459        // Business rule: cannot delete if borrowed
460        if object.is_borrowed() {
461            return Err("Cannot delete object while it is borrowed".to_string());
462        }
463
464        // Delete object
465        self.shared_object_repo.delete(object_id).await?;
466
467        Ok(())
468    }
469
470    /// Get shared object statistics for a building
471    pub async fn get_object_statistics(
472        &self,
473        building_id: Uuid,
474    ) -> Result<SharedObjectStatisticsDto, String> {
475        let total_objects = self
476            .shared_object_repo
477            .count_by_building(building_id)
478            .await?;
479        let available_objects = self
480            .shared_object_repo
481            .count_available_by_building(building_id)
482            .await?;
483        let borrowed_objects = self
484            .shared_object_repo
485            .count_borrowed_by_building(building_id)
486            .await?;
487        let overdue_objects = self
488            .shared_object_repo
489            .count_overdue_by_building(building_id)
490            .await?;
491
492        // Calculate free/paid objects
493        let objects = self
494            .shared_object_repo
495            .find_by_building(building_id)
496            .await?;
497        let free_objects = objects.iter().filter(|o| o.is_free()).count() as i64;
498        let paid_objects = total_objects - free_objects;
499
500        // Count by category
501        let mut objects_by_category = Vec::new();
502        for category in [
503            SharedObjectCategory::Tools,
504            SharedObjectCategory::Books,
505            SharedObjectCategory::Electronics,
506            SharedObjectCategory::Sports,
507            SharedObjectCategory::Gardening,
508            SharedObjectCategory::Kitchen,
509            SharedObjectCategory::Baby,
510            SharedObjectCategory::Other,
511        ] {
512            let count = self
513                .shared_object_repo
514                .count_by_category(building_id, category.clone())
515                .await?;
516            if count > 0 {
517                objects_by_category.push(CategoryObjectCount { category, count });
518            }
519        }
520
521        Ok(SharedObjectStatisticsDto {
522            total_objects,
523            available_objects,
524            borrowed_objects,
525            overdue_objects,
526            free_objects,
527            paid_objects,
528            objects_by_category,
529        })
530    }
531
532    /// Helper method to enrich objects with owner names
533    async fn enrich_objects_summary(
534        &self,
535        objects: Vec<SharedObject>,
536    ) -> Result<Vec<SharedObjectSummaryDto>, String> {
537        let mut enriched = Vec::new();
538
539        for object in objects {
540            // Get owner name
541            let owner = self.owner_repo.find_by_id(object.owner_id).await?;
542            let owner_name = if let Some(owner) = owner {
543                format!("{} {}", owner.first_name, owner.last_name)
544            } else {
545                "Unknown Owner".to_string()
546            };
547
548            enriched.push(SharedObjectSummaryDto::from_shared_object(
549                object, owner_name,
550            ));
551        }
552
553        Ok(enriched)
554    }
555}