koprogo_api/application/use_cases/
shared_object_use_cases.rs1use 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 pub async fn create_shared_object(
36 &self,
37 owner_id: Uuid,
38 dto: CreateSharedObjectDto,
39 ) -> Result<SharedObjectResponseDto, String> {
40 let owner = self
42 .owner_repo
43 .find_by_id(owner_id)
44 .await?
45 .ok_or("Owner not found".to_string())?;
46
47 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 let created = self.shared_object_repo.create(&object).await?;
66
67 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 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 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 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 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 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 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 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 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 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 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 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 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 if object.owner_id != actor_id {
227 return Err("Unauthorized: only owner can update object".to_string());
228 }
229
230 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 let updated = self.shared_object_repo.update(&object).await?;
246
247 self.get_shared_object(updated.id).await
249 }
250
251 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 if object.owner_id != actor_id {
268 return Err("Unauthorized: only owner can mark object as available".to_string());
269 }
270
271 object.mark_available()?;
273
274 let updated = self.shared_object_repo.update(&object).await?;
276
277 self.get_shared_object(updated.id).await
279 }
280
281 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 if object.owner_id != actor_id {
298 return Err("Unauthorized: only owner can mark object as unavailable".to_string());
299 }
300
301 object.mark_unavailable();
303
304 let updated = self.shared_object_repo.update(&object).await?;
306
307 self.get_shared_object(updated.id).await
309 }
310
311 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 if let Some(deposit_amount) = object.deposit_credits {
334 if deposit_amount > 0 {
335 let mut borrower_balance = self
337 .credit_balance_repo
338 .get_or_create(borrower_id, object.building_id)
339 .await?;
340
341 let new_balance = borrower_balance.balance - deposit_amount;
344 if new_balance < -100 {
345 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 borrower_balance.spend_credits(deposit_amount)?;
355
356 self.credit_balance_repo.update(&borrower_balance).await?;
358 }
359 }
360
361 object.borrow(borrower_id, dto.duration_days)?;
363
364 let updated = self.shared_object_repo.update(&object).await?;
366
367 self.get_shared_object(updated.id).await
369 }
370
371 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 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 let mut borrower_balance = self
402 .credit_balance_repo
403 .get_or_create(borrower_id, object.building_id)
404 .await?;
405
406 let mut owner_balance = self
408 .credit_balance_repo
409 .get_or_create(owner_id, object.building_id)
410 .await?;
411
412 if rental_cost > 0 {
414 borrower_balance.spend_credits(rental_cost)?;
415 owner_balance.earn_credits(rental_cost)?;
416 }
417
418 if deposit > 0 {
420 borrower_balance.earn_credits(deposit)?;
421 }
422
423 self.credit_balance_repo.update(&borrower_balance).await?;
425 self.credit_balance_repo.update(&owner_balance).await?;
426 }
427
428 object.return_object(returner_id)?;
430
431 let updated = self.shared_object_repo.update(&object).await?;
433
434 self.get_shared_object(updated.id).await
436 }
437
438 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 if object.owner_id != actor_id {
456 return Err("Unauthorized: only owner can delete object".to_string());
457 }
458
459 if object.is_borrowed() {
461 return Err("Cannot delete object while it is borrowed".to_string());
462 }
463
464 self.shared_object_repo.delete(object_id).await?;
466
467 Ok(())
468 }
469
470 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 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 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 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 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}