1use crate::application::dto::{
2 CancelExchangeDto, CompleteExchangeDto, CreateLocalExchangeDto, LocalExchangeResponseDto,
3 OwnerCreditBalanceDto, OwnerExchangeSummaryDto, RateExchangeDto, RequestExchangeDto,
4 SelStatisticsDto,
5};
6use crate::application::ports::{
7 LocalExchangeRepository, OwnerCreditBalanceRepository, OwnerRepository,
8};
9use crate::domain::entities::{ExchangeStatus, ExchangeType, LocalExchange};
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub struct LocalExchangeUseCases {
15 exchange_repo: Arc<dyn LocalExchangeRepository>,
16 balance_repo: Arc<dyn OwnerCreditBalanceRepository>,
17 owner_repo: Arc<dyn OwnerRepository>,
18}
19
20impl LocalExchangeUseCases {
21 pub fn new(
22 exchange_repo: Arc<dyn LocalExchangeRepository>,
23 balance_repo: Arc<dyn OwnerCreditBalanceRepository>,
24 owner_repo: Arc<dyn OwnerRepository>,
25 ) -> Self {
26 Self {
27 exchange_repo,
28 balance_repo,
29 owner_repo,
30 }
31 }
32
33 async fn resolve_owner_id(&self, user_id: Uuid) -> Result<Uuid, String> {
35 let owner = self
36 .owner_repo
37 .find_by_user_id(user_id)
38 .await?
39 .ok_or("User is not linked to an owner account".to_string())?;
40 Ok(owner.id)
41 }
42
43 pub async fn create_exchange(
45 &self,
46 user_id: Uuid, dto: CreateLocalExchangeDto,
48 ) -> Result<LocalExchangeResponseDto, String> {
49 let provider = self
51 .owner_repo
52 .find_by_user_id(user_id)
53 .await?
54 .ok_or("Provider not found - user is not linked to an owner account".to_string())?;
55
56 let exchange = LocalExchange::new(
58 dto.building_id,
59 provider.id,
60 dto.exchange_type,
61 dto.title,
62 dto.description,
63 dto.credits,
64 )?;
65
66 let created = self.exchange_repo.create(&exchange).await?;
68
69 Ok(LocalExchangeResponseDto::from_entity(
71 created,
72 format!("{} {}", provider.first_name, provider.last_name),
73 None,
74 ))
75 }
76
77 pub async fn get_exchange(&self, id: Uuid) -> Result<LocalExchangeResponseDto, String> {
79 let exchange = self
80 .exchange_repo
81 .find_by_id(id)
82 .await?
83 .ok_or("Exchange not found".to_string())?;
84
85 let provider = self
87 .owner_repo
88 .find_by_id(exchange.provider_id)
89 .await?
90 .ok_or("Provider not found".to_string())?;
91 let provider_name = format!("{} {}", provider.first_name, provider.last_name);
92
93 let requester_name = if let Some(requester_id) = exchange.requester_id {
95 let requester = self.owner_repo.find_by_id(requester_id).await?;
96 requester.map(|r| format!("{} {}", r.first_name, r.last_name))
97 } else {
98 None
99 };
100
101 Ok(LocalExchangeResponseDto::from_entity(
102 exchange,
103 provider_name,
104 requester_name,
105 ))
106 }
107
108 pub async fn list_building_exchanges(
110 &self,
111 building_id: Uuid,
112 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
113 let exchanges = self.exchange_repo.find_by_building(building_id).await?;
114
115 self.enrich_exchanges_with_names(exchanges).await
116 }
117
118 pub async fn list_available_exchanges(
120 &self,
121 building_id: Uuid,
122 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
123 let exchanges = self
124 .exchange_repo
125 .find_available_by_building(building_id)
126 .await?;
127
128 self.enrich_exchanges_with_names(exchanges).await
129 }
130
131 pub async fn list_owner_exchanges(
133 &self,
134 owner_id: Uuid,
135 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
136 let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
137
138 self.enrich_exchanges_with_names(exchanges).await
139 }
140
141 pub async fn list_exchanges_by_type(
143 &self,
144 building_id: Uuid,
145 exchange_type: ExchangeType,
146 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
147 let exchanges = self
148 .exchange_repo
149 .find_by_type(building_id, exchange_type.to_sql())
150 .await?;
151
152 self.enrich_exchanges_with_names(exchanges).await
153 }
154
155 pub async fn request_exchange(
157 &self,
158 exchange_id: Uuid,
159 user_id: Uuid, _dto: RequestExchangeDto,
161 ) -> Result<LocalExchangeResponseDto, String> {
162 let owner_id = self.resolve_owner_id(user_id).await?;
163
164 let mut exchange = self
166 .exchange_repo
167 .find_by_id(exchange_id)
168 .await?
169 .ok_or("Exchange not found".to_string())?;
170
171 exchange.request(owner_id)?;
173
174 let updated = self.exchange_repo.update(&exchange).await?;
176
177 self.get_exchange(updated.id).await
179 }
180
181 pub async fn start_exchange(
184 &self,
185 exchange_id: Uuid,
186 user_id: Uuid, ) -> Result<LocalExchangeResponseDto, String> {
188 let owner_id = self.resolve_owner_id(user_id).await?;
189
190 let mut exchange = self
191 .exchange_repo
192 .find_by_id(exchange_id)
193 .await?
194 .ok_or("Exchange not found".to_string())?;
195
196 exchange.start(owner_id)?;
197
198 let updated = self.exchange_repo.update(&exchange).await?;
199
200 self.get_exchange(updated.id).await
201 }
202
203 pub async fn complete_exchange(
206 &self,
207 exchange_id: Uuid,
208 user_id: Uuid, _dto: CompleteExchangeDto,
210 ) -> Result<LocalExchangeResponseDto, String> {
211 let owner_id = self.resolve_owner_id(user_id).await?;
212
213 let mut exchange = self
214 .exchange_repo
215 .find_by_id(exchange_id)
216 .await?
217 .ok_or("Exchange not found".to_string())?;
218
219 let requester_id = exchange
221 .requester_id
222 .ok_or("Exchange has no requester".to_string())?;
223
224 exchange.complete(owner_id)?;
225
226 let updated = self.exchange_repo.update(&exchange).await?;
227
228 self.update_credit_balances_on_completion(&updated).await?;
230
231 let mut provider_balance = self
233 .balance_repo
234 .get_or_create(updated.provider_id, updated.building_id)
235 .await?;
236 provider_balance.increment_exchanges();
237 self.balance_repo.update(&provider_balance).await?;
238
239 let mut requester_balance = self
240 .balance_repo
241 .get_or_create(requester_id, updated.building_id)
242 .await?;
243 requester_balance.increment_exchanges();
244 self.balance_repo.update(&requester_balance).await?;
245
246 self.get_exchange(updated.id).await
247 }
248
249 pub async fn cancel_exchange(
251 &self,
252 exchange_id: Uuid,
253 user_id: Uuid, dto: CancelExchangeDto,
255 ) -> Result<LocalExchangeResponseDto, String> {
256 let owner_id = self.resolve_owner_id(user_id).await?;
257
258 let mut exchange = self
259 .exchange_repo
260 .find_by_id(exchange_id)
261 .await?
262 .ok_or("Exchange not found".to_string())?;
263
264 exchange.cancel(owner_id, dto.reason)?;
265
266 let updated = self.exchange_repo.update(&exchange).await?;
267
268 self.get_exchange(updated.id).await
269 }
270
271 pub async fn rate_provider(
273 &self,
274 exchange_id: Uuid,
275 user_id: Uuid, dto: RateExchangeDto,
277 ) -> Result<LocalExchangeResponseDto, String> {
278 let owner_id = self.resolve_owner_id(user_id).await?;
279
280 let mut exchange = self
281 .exchange_repo
282 .find_by_id(exchange_id)
283 .await?
284 .ok_or("Exchange not found".to_string())?;
285
286 exchange.rate_provider(owner_id, dto.rating)?;
287
288 let updated = self.exchange_repo.update(&exchange).await?;
289
290 self.update_average_rating(updated.provider_id, updated.building_id)
292 .await?;
293
294 self.get_exchange(updated.id).await
295 }
296
297 pub async fn rate_requester(
299 &self,
300 exchange_id: Uuid,
301 user_id: Uuid, dto: RateExchangeDto,
303 ) -> Result<LocalExchangeResponseDto, String> {
304 let owner_id = self.resolve_owner_id(user_id).await?;
305
306 let mut exchange = self
307 .exchange_repo
308 .find_by_id(exchange_id)
309 .await?
310 .ok_or("Exchange not found".to_string())?;
311
312 exchange.rate_requester(owner_id, dto.rating)?;
313
314 let updated = self.exchange_repo.update(&exchange).await?;
315
316 if let Some(requester_id) = updated.requester_id {
318 self.update_average_rating(requester_id, updated.building_id)
319 .await?;
320 }
321
322 self.get_exchange(updated.id).await
323 }
324
325 pub async fn delete_exchange(&self, exchange_id: Uuid, user_id: Uuid) -> Result<(), String> {
327 let owner_id = self.resolve_owner_id(user_id).await?;
328
329 let exchange = self
330 .exchange_repo
331 .find_by_id(exchange_id)
332 .await?
333 .ok_or("Exchange not found".to_string())?;
334
335 if exchange.provider_id != owner_id {
337 return Err("Only the provider can delete the exchange".to_string());
338 }
339
340 if exchange.status == ExchangeStatus::Completed {
342 return Err("Cannot delete a completed exchange".to_string());
343 }
344
345 self.exchange_repo.delete(exchange_id).await?;
346
347 Ok(())
348 }
349
350 pub async fn get_credit_balance(
352 &self,
353 owner_id: Uuid,
354 building_id: Uuid,
355 ) -> Result<OwnerCreditBalanceDto, String> {
356 let balance = self
357 .balance_repo
358 .get_or_create(owner_id, building_id)
359 .await?;
360
361 let owner = self
362 .owner_repo
363 .find_by_id(owner_id)
364 .await?
365 .ok_or("Owner not found".to_string())?;
366 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
367
368 Ok(OwnerCreditBalanceDto::from_entity(balance, owner_name))
369 }
370
371 pub async fn get_leaderboard(
373 &self,
374 building_id: Uuid,
375 limit: i32,
376 ) -> Result<Vec<OwnerCreditBalanceDto>, String> {
377 let balances = self
378 .balance_repo
379 .get_leaderboard(building_id, limit)
380 .await?;
381
382 let mut dtos = Vec::new();
383 for balance in balances {
384 if let Some(owner) = self.owner_repo.find_by_id(balance.owner_id).await? {
385 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
386 dtos.push(OwnerCreditBalanceDto::from_entity(balance, owner_name));
387 }
388 }
389
390 Ok(dtos)
391 }
392
393 pub async fn get_statistics(&self, building_id: Uuid) -> Result<SelStatisticsDto, String> {
395 let total_exchanges = self.exchange_repo.count_by_building(building_id).await? as i32;
396
397 let active_exchanges = self
398 .exchange_repo
399 .count_by_building_and_status(building_id, ExchangeStatus::Offered.to_sql())
400 .await? as i32
401 + self
402 .exchange_repo
403 .count_by_building_and_status(building_id, ExchangeStatus::Requested.to_sql())
404 .await? as i32
405 + self
406 .exchange_repo
407 .count_by_building_and_status(building_id, ExchangeStatus::InProgress.to_sql())
408 .await? as i32;
409
410 let completed_exchanges = self
411 .exchange_repo
412 .count_by_building_and_status(building_id, ExchangeStatus::Completed.to_sql())
413 .await? as i32;
414
415 let total_credits_exchanged = self
416 .exchange_repo
417 .get_total_credits_exchanged(building_id)
418 .await?;
419
420 let active_participants = self
421 .balance_repo
422 .count_active_participants(building_id)
423 .await? as i32;
424
425 let average_exchange_rating = self
426 .exchange_repo
427 .get_average_exchange_rating(building_id)
428 .await?;
429
430 let service_count = self
432 .exchange_repo
433 .count_by_building_and_type(building_id, ExchangeType::Service.to_sql())
434 .await?;
435 let object_loan_count = self
436 .exchange_repo
437 .count_by_building_and_type(building_id, ExchangeType::ObjectLoan.to_sql())
438 .await?;
439 let shared_purchase_count = self
440 .exchange_repo
441 .count_by_building_and_type(building_id, ExchangeType::SharedPurchase.to_sql())
442 .await?;
443
444 let most_popular_exchange_type =
445 if service_count >= object_loan_count && service_count >= shared_purchase_count {
446 Some(ExchangeType::Service)
447 } else if object_loan_count >= shared_purchase_count {
448 Some(ExchangeType::ObjectLoan)
449 } else {
450 Some(ExchangeType::SharedPurchase)
451 };
452
453 Ok(SelStatisticsDto {
454 building_id,
455 total_exchanges,
456 active_exchanges,
457 completed_exchanges,
458 total_credits_exchanged,
459 active_participants,
460 average_exchange_rating,
461 most_popular_exchange_type,
462 })
463 }
464
465 pub async fn get_owner_summary(
467 &self,
468 owner_id: Uuid,
469 ) -> Result<OwnerExchangeSummaryDto, String> {
470 let owner = self
471 .owner_repo
472 .find_by_id(owner_id)
473 .await?
474 .ok_or("Owner not found".to_string())?;
475 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
476
477 let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
478
479 let as_provider = exchanges
480 .iter()
481 .filter(|e| e.provider_id == owner_id)
482 .count() as i32;
483
484 let as_requester = exchanges
485 .iter()
486 .filter(|e| e.requester_id == Some(owner_id))
487 .count() as i32;
488
489 let mut total_ratings = 0;
491 let mut rating_count = 0;
492
493 for exchange in &exchanges {
494 if exchange.provider_id == owner_id {
495 if let Some(rating) = exchange.provider_rating {
496 total_ratings += rating;
497 rating_count += 1;
498 }
499 }
500 if exchange.requester_id == Some(owner_id) {
501 if let Some(rating) = exchange.requester_rating {
502 total_ratings += rating;
503 rating_count += 1;
504 }
505 }
506 }
507
508 let average_rating = if rating_count > 0 {
509 Some(total_ratings as f32 / rating_count as f32)
510 } else {
511 None
512 };
513
514 let recent: Vec<_> = exchanges.into_iter().take(5).collect();
516 let recent_dtos = self.enrich_exchanges_with_names(recent).await?;
517
518 Ok(OwnerExchangeSummaryDto {
519 owner_id,
520 owner_name,
521 as_provider,
522 as_requester,
523 total_exchanges: as_provider + as_requester,
524 average_rating,
525 recent_exchanges: recent_dtos,
526 })
527 }
528
529 async fn update_credit_balances_on_completion(
533 &self,
534 exchange: &LocalExchange,
535 ) -> Result<(), String> {
536 let requester_id = exchange
537 .requester_id
538 .ok_or("Exchange has no requester".to_string())?;
539
540 let mut provider_balance = self
542 .balance_repo
543 .get_or_create(exchange.provider_id, exchange.building_id)
544 .await?;
545 provider_balance.earn_credits(exchange.credits)?;
546 self.balance_repo.update(&provider_balance).await?;
547
548 let mut requester_balance = self
550 .balance_repo
551 .get_or_create(requester_id, exchange.building_id)
552 .await?;
553 requester_balance.spend_credits(exchange.credits)?;
554 self.balance_repo.update(&requester_balance).await?;
555
556 Ok(())
557 }
558
559 async fn update_average_rating(&self, owner_id: Uuid, building_id: Uuid) -> Result<(), String> {
561 let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
562
563 let mut total_ratings = 0;
564 let mut rating_count = 0;
565
566 for exchange in exchanges {
567 if exchange.provider_id == owner_id {
568 if let Some(rating) = exchange.provider_rating {
569 total_ratings += rating;
570 rating_count += 1;
571 }
572 }
573 if exchange.requester_id == Some(owner_id) {
574 if let Some(rating) = exchange.requester_rating {
575 total_ratings += rating;
576 rating_count += 1;
577 }
578 }
579 }
580
581 if rating_count > 0 {
582 let average = total_ratings as f32 / rating_count as f32;
583
584 let mut balance = self
585 .balance_repo
586 .get_or_create(owner_id, building_id)
587 .await?;
588 balance.update_rating(average)?;
589 self.balance_repo.update(&balance).await?;
590 }
591
592 Ok(())
593 }
594
595 async fn enrich_exchanges_with_names(
597 &self,
598 exchanges: Vec<LocalExchange>,
599 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
600 let mut dtos = Vec::new();
601
602 for exchange in exchanges {
603 let provider = self.owner_repo.find_by_id(exchange.provider_id).await?;
605 let provider_name = if let Some(p) = provider {
606 format!("{} {}", p.first_name, p.last_name)
607 } else {
608 "Unknown".to_string()
609 };
610
611 let requester_name = if let Some(requester_id) = exchange.requester_id {
613 let requester = self.owner_repo.find_by_id(requester_id).await?;
614 requester.map(|r| format!("{} {}", r.first_name, r.last_name))
615 } else {
616 None
617 };
618
619 dtos.push(LocalExchangeResponseDto::from_entity(
620 exchange,
621 provider_name,
622 requester_name,
623 ));
624 }
625
626 Ok(dtos)
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use crate::application::dto::{OwnerFilters, PageRequest};
634 use crate::application::ports::{
635 LocalExchangeRepository, OwnerCreditBalanceRepository, OwnerRepository,
636 };
637 use crate::domain::entities::{
638 ExchangeStatus, ExchangeType, LocalExchange, Owner, OwnerCreditBalance,
639 };
640 use async_trait::async_trait;
641 use chrono::Utc;
642 use std::collections::HashMap;
643 use std::sync::Mutex;
644
645 struct MockOwnerRepository {
648 owners: Mutex<HashMap<Uuid, Owner>>,
649 }
650
651 impl MockOwnerRepository {
652 fn new() -> Self {
653 Self {
654 owners: Mutex::new(HashMap::new()),
655 }
656 }
657
658 fn insert(&self, owner: Owner) {
659 self.owners.lock().unwrap().insert(owner.id, owner);
660 }
661 }
662
663 #[async_trait]
664 impl OwnerRepository for MockOwnerRepository {
665 async fn create(&self, owner: &Owner) -> Result<Owner, String> {
666 self.owners.lock().unwrap().insert(owner.id, owner.clone());
667 Ok(owner.clone())
668 }
669
670 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
671 Ok(self.owners.lock().unwrap().get(&id).cloned())
672 }
673
674 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
675 Ok(self
676 .owners
677 .lock()
678 .unwrap()
679 .values()
680 .find(|o| o.user_id == Some(user_id))
681 .cloned())
682 }
683
684 async fn find_by_user_id_and_organization(
685 &self,
686 user_id: Uuid,
687 organization_id: Uuid,
688 ) -> Result<Option<Owner>, String> {
689 Ok(self
690 .owners
691 .lock()
692 .unwrap()
693 .values()
694 .find(|o| o.user_id == Some(user_id) && o.organization_id == organization_id)
695 .cloned())
696 }
697
698 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
699 Ok(self
700 .owners
701 .lock()
702 .unwrap()
703 .values()
704 .find(|o| o.email == email)
705 .cloned())
706 }
707
708 async fn find_all(&self) -> Result<Vec<Owner>, String> {
709 Ok(self.owners.lock().unwrap().values().cloned().collect())
710 }
711
712 async fn find_all_paginated(
713 &self,
714 _page_request: &PageRequest,
715 _filters: &OwnerFilters,
716 ) -> Result<(Vec<Owner>, i64), String> {
717 let owners: Vec<_> = self.owners.lock().unwrap().values().cloned().collect();
718 let total = owners.len() as i64;
719 Ok((owners, total))
720 }
721
722 async fn update(&self, owner: &Owner) -> Result<Owner, String> {
723 self.owners.lock().unwrap().insert(owner.id, owner.clone());
724 Ok(owner.clone())
725 }
726
727 async fn delete(&self, id: Uuid) -> Result<bool, String> {
728 Ok(self.owners.lock().unwrap().remove(&id).is_some())
729 }
730 async fn set_user_link(
731 &self,
732 owner_id: Uuid,
733 user_id: Option<Uuid>,
734 ) -> Result<bool, String> {
735 let mut map = self.owners.lock().unwrap();
736 if let Some(o) = map.get_mut(&owner_id) {
737 o.user_id = user_id;
738 Ok(true)
739 } else {
740 Ok(false)
741 }
742 }
743 }
744
745 struct MockLocalExchangeRepository {
748 exchanges: Mutex<HashMap<Uuid, LocalExchange>>,
749 }
750
751 impl MockLocalExchangeRepository {
752 fn new() -> Self {
753 Self {
754 exchanges: Mutex::new(HashMap::new()),
755 }
756 }
757 }
758
759 #[async_trait]
760 impl LocalExchangeRepository for MockLocalExchangeRepository {
761 async fn create(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
762 self.exchanges
763 .lock()
764 .unwrap()
765 .insert(exchange.id, exchange.clone());
766 Ok(exchange.clone())
767 }
768
769 async fn find_by_id(&self, id: Uuid) -> Result<Option<LocalExchange>, String> {
770 Ok(self.exchanges.lock().unwrap().get(&id).cloned())
771 }
772
773 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<LocalExchange>, String> {
774 Ok(self
775 .exchanges
776 .lock()
777 .unwrap()
778 .values()
779 .filter(|e| e.building_id == building_id)
780 .cloned()
781 .collect())
782 }
783
784 async fn find_by_building_and_status(
785 &self,
786 building_id: Uuid,
787 status: &str,
788 ) -> Result<Vec<LocalExchange>, String> {
789 Ok(self
790 .exchanges
791 .lock()
792 .unwrap()
793 .values()
794 .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
795 .cloned()
796 .collect())
797 }
798
799 async fn find_by_provider(&self, provider_id: Uuid) -> Result<Vec<LocalExchange>, String> {
800 Ok(self
801 .exchanges
802 .lock()
803 .unwrap()
804 .values()
805 .filter(|e| e.provider_id == provider_id)
806 .cloned()
807 .collect())
808 }
809
810 async fn find_by_requester(
811 &self,
812 requester_id: Uuid,
813 ) -> Result<Vec<LocalExchange>, String> {
814 Ok(self
815 .exchanges
816 .lock()
817 .unwrap()
818 .values()
819 .filter(|e| e.requester_id == Some(requester_id))
820 .cloned()
821 .collect())
822 }
823
824 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<LocalExchange>, String> {
825 Ok(self
826 .exchanges
827 .lock()
828 .unwrap()
829 .values()
830 .filter(|e| e.provider_id == owner_id || e.requester_id == Some(owner_id))
831 .cloned()
832 .collect())
833 }
834
835 async fn find_active_by_building(
836 &self,
837 building_id: Uuid,
838 ) -> Result<Vec<LocalExchange>, String> {
839 Ok(self
840 .exchanges
841 .lock()
842 .unwrap()
843 .values()
844 .filter(|e| e.building_id == building_id && e.is_active())
845 .cloned()
846 .collect())
847 }
848
849 async fn find_available_by_building(
850 &self,
851 building_id: Uuid,
852 ) -> Result<Vec<LocalExchange>, String> {
853 Ok(self
854 .exchanges
855 .lock()
856 .unwrap()
857 .values()
858 .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Offered)
859 .cloned()
860 .collect())
861 }
862
863 async fn find_by_type(
864 &self,
865 building_id: Uuid,
866 exchange_type: &str,
867 ) -> Result<Vec<LocalExchange>, String> {
868 Ok(self
869 .exchanges
870 .lock()
871 .unwrap()
872 .values()
873 .filter(|e| {
874 e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
875 })
876 .cloned()
877 .collect())
878 }
879
880 async fn update(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
881 self.exchanges
882 .lock()
883 .unwrap()
884 .insert(exchange.id, exchange.clone());
885 Ok(exchange.clone())
886 }
887
888 async fn delete(&self, id: Uuid) -> Result<bool, String> {
889 Ok(self.exchanges.lock().unwrap().remove(&id).is_some())
890 }
891
892 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
893 Ok(self
894 .exchanges
895 .lock()
896 .unwrap()
897 .values()
898 .filter(|e| e.building_id == building_id)
899 .count() as i64)
900 }
901
902 async fn count_by_building_and_status(
903 &self,
904 building_id: Uuid,
905 status: &str,
906 ) -> Result<i64, String> {
907 Ok(self
908 .exchanges
909 .lock()
910 .unwrap()
911 .values()
912 .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
913 .count() as i64)
914 }
915
916 async fn count_by_building_and_type(
917 &self,
918 building_id: Uuid,
919 exchange_type: &str,
920 ) -> Result<i64, String> {
921 Ok(self
922 .exchanges
923 .lock()
924 .unwrap()
925 .values()
926 .filter(|e| {
927 e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
928 })
929 .count() as i64)
930 }
931
932 async fn get_total_credits_exchanged(&self, building_id: Uuid) -> Result<i32, String> {
933 Ok(self
934 .exchanges
935 .lock()
936 .unwrap()
937 .values()
938 .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Completed)
939 .map(|e| e.credits)
940 .sum())
941 }
942
943 async fn get_average_exchange_rating(
944 &self,
945 building_id: Uuid,
946 ) -> Result<Option<f32>, String> {
947 let ratings: Vec<f32> = self
948 .exchanges
949 .lock()
950 .unwrap()
951 .values()
952 .filter(|e| e.building_id == building_id)
953 .flat_map(|e| {
954 let mut v = Vec::new();
955 if let Some(r) = e.provider_rating {
956 v.push(r as f32);
957 }
958 if let Some(r) = e.requester_rating {
959 v.push(r as f32);
960 }
961 v
962 })
963 .collect();
964 if ratings.is_empty() {
965 Ok(None)
966 } else {
967 Ok(Some(ratings.iter().sum::<f32>() / ratings.len() as f32))
968 }
969 }
970 }
971
972 struct MockOwnerCreditBalanceRepository {
975 balances: Mutex<HashMap<(Uuid, Uuid), OwnerCreditBalance>>,
976 }
977
978 impl MockOwnerCreditBalanceRepository {
979 fn new() -> Self {
980 Self {
981 balances: Mutex::new(HashMap::new()),
982 }
983 }
984 }
985
986 #[async_trait]
987 impl OwnerCreditBalanceRepository for MockOwnerCreditBalanceRepository {
988 async fn create(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
989 self.balances
990 .lock()
991 .unwrap()
992 .insert((balance.owner_id, balance.building_id), balance.clone());
993 Ok(balance.clone())
994 }
995
996 async fn find_by_owner_and_building(
997 &self,
998 owner_id: Uuid,
999 building_id: Uuid,
1000 ) -> Result<Option<OwnerCreditBalance>, String> {
1001 Ok(self
1002 .balances
1003 .lock()
1004 .unwrap()
1005 .get(&(owner_id, building_id))
1006 .cloned())
1007 }
1008
1009 async fn find_by_building(
1010 &self,
1011 building_id: Uuid,
1012 ) -> Result<Vec<OwnerCreditBalance>, String> {
1013 Ok(self
1014 .balances
1015 .lock()
1016 .unwrap()
1017 .values()
1018 .filter(|b| b.building_id == building_id)
1019 .cloned()
1020 .collect())
1021 }
1022
1023 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerCreditBalance>, String> {
1024 Ok(self
1025 .balances
1026 .lock()
1027 .unwrap()
1028 .values()
1029 .filter(|b| b.owner_id == owner_id)
1030 .cloned()
1031 .collect())
1032 }
1033
1034 async fn get_or_create(
1035 &self,
1036 owner_id: Uuid,
1037 building_id: Uuid,
1038 ) -> Result<OwnerCreditBalance, String> {
1039 let mut map = self.balances.lock().unwrap();
1040 if let Some(existing) = map.get(&(owner_id, building_id)) {
1041 Ok(existing.clone())
1042 } else {
1043 let balance = OwnerCreditBalance::new(owner_id, building_id);
1044 map.insert((owner_id, building_id), balance.clone());
1045 Ok(balance)
1046 }
1047 }
1048
1049 async fn update(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
1050 self.balances
1051 .lock()
1052 .unwrap()
1053 .insert((balance.owner_id, balance.building_id), balance.clone());
1054 Ok(balance.clone())
1055 }
1056
1057 async fn delete(&self, owner_id: Uuid, building_id: Uuid) -> Result<bool, String> {
1058 Ok(self
1059 .balances
1060 .lock()
1061 .unwrap()
1062 .remove(&(owner_id, building_id))
1063 .is_some())
1064 }
1065
1066 async fn get_leaderboard(
1067 &self,
1068 building_id: Uuid,
1069 limit: i32,
1070 ) -> Result<Vec<OwnerCreditBalance>, String> {
1071 let mut balances: Vec<_> = self
1072 .balances
1073 .lock()
1074 .unwrap()
1075 .values()
1076 .filter(|b| b.building_id == building_id)
1077 .cloned()
1078 .collect();
1079 balances.sort_by_key(|a| std::cmp::Reverse(a.balance));
1080 balances.truncate(limit as usize);
1081 Ok(balances)
1082 }
1083
1084 async fn count_active_participants(&self, building_id: Uuid) -> Result<i64, String> {
1085 Ok(self
1086 .balances
1087 .lock()
1088 .unwrap()
1089 .values()
1090 .filter(|b| b.building_id == building_id && b.total_exchanges > 0)
1091 .count() as i64)
1092 }
1093
1094 async fn get_total_credits_in_circulation(&self, building_id: Uuid) -> Result<i32, String> {
1095 Ok(self
1096 .balances
1097 .lock()
1098 .unwrap()
1099 .values()
1100 .filter(|b| b.building_id == building_id)
1101 .map(|b| b.credits_earned)
1102 .sum())
1103 }
1104 }
1105
1106 fn make_owner(user_id: Uuid) -> Owner {
1109 let now = Utc::now();
1110 Owner {
1111 id: Uuid::new_v4(),
1112 organization_id: Uuid::new_v4(),
1113 user_id: Some(user_id),
1114 first_name: "Jean".to_string(),
1115 last_name: "Dupont".to_string(),
1116 email: "jean@example.com".to_string(),
1117 phone: None,
1118 address: "1 rue de la Loi".to_string(),
1119 city: "Bruxelles".to_string(),
1120 postal_code: "1000".to_string(),
1121 country: "BE".to_string(),
1122 created_at: now,
1123 updated_at: now,
1124 }
1125 }
1126
1127 fn make_owner_with_name(user_id: Uuid, first: &str, last: &str) -> Owner {
1128 let now = Utc::now();
1129 Owner {
1130 id: Uuid::new_v4(),
1131 organization_id: Uuid::new_v4(),
1132 user_id: Some(user_id),
1133 first_name: first.to_string(),
1134 last_name: last.to_string(),
1135 email: format!("{}@example.com", first.to_lowercase()),
1136 phone: None,
1137 address: "1 rue de la Loi".to_string(),
1138 city: "Bruxelles".to_string(),
1139 postal_code: "1000".to_string(),
1140 country: "BE".to_string(),
1141 created_at: now,
1142 updated_at: now,
1143 }
1144 }
1145
1146 fn setup_use_cases(
1147 owner_repo: Arc<MockOwnerRepository>,
1148 exchange_repo: Arc<MockLocalExchangeRepository>,
1149 balance_repo: Arc<MockOwnerCreditBalanceRepository>,
1150 ) -> LocalExchangeUseCases {
1151 LocalExchangeUseCases::new(exchange_repo, balance_repo, owner_repo)
1152 }
1153
1154 #[tokio::test]
1157 async fn test_create_exchange_success() {
1158 let owner_repo = Arc::new(MockOwnerRepository::new());
1159 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1160 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1161
1162 let user_id = Uuid::new_v4();
1163 let provider = make_owner_with_name(user_id, "Alice", "Martin");
1164 owner_repo.insert(provider.clone());
1165
1166 let building_id = Uuid::new_v4();
1167 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1168
1169 let dto = CreateLocalExchangeDto {
1170 building_id,
1171 exchange_type: ExchangeType::Service,
1172 title: "Gardening help".to_string(),
1173 description: "I can help with your garden".to_string(),
1174 credits: 3,
1175 };
1176
1177 let result = uc.create_exchange(user_id, dto).await;
1178 assert!(result.is_ok(), "create_exchange failed: {:?}", result.err());
1179
1180 let resp = result.unwrap();
1181 assert_eq!(resp.building_id, building_id);
1182 assert_eq!(resp.provider_id, provider.id);
1183 assert_eq!(resp.provider_name, "Alice Martin");
1184 assert_eq!(resp.status, ExchangeStatus::Offered);
1185 assert_eq!(resp.credits, 3);
1186 assert_eq!(resp.title, "Gardening help");
1187 assert!(resp.requester_id.is_none());
1188 }
1189
1190 #[tokio::test]
1191 async fn test_request_exchange_success() {
1192 let owner_repo = Arc::new(MockOwnerRepository::new());
1193 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1194 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1195
1196 let provider_user_id = Uuid::new_v4();
1197 let requester_user_id = Uuid::new_v4();
1198 let building_id = Uuid::new_v4();
1199
1200 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1201 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1202 owner_repo.insert(provider.clone());
1203 owner_repo.insert(requester.clone());
1204
1205 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1206
1207 let dto = CreateLocalExchangeDto {
1209 building_id,
1210 exchange_type: ExchangeType::ObjectLoan,
1211 title: "Drill to lend".to_string(),
1212 description: "Heavy-duty drill available".to_string(),
1213 credits: 1,
1214 };
1215 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1216
1217 let result = uc
1219 .request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1220 .await;
1221 assert!(
1222 result.is_ok(),
1223 "request_exchange failed: {:?}",
1224 result.err()
1225 );
1226
1227 let resp = result.unwrap();
1228 assert_eq!(resp.status, ExchangeStatus::Requested);
1229 assert_eq!(resp.requester_id, Some(requester.id));
1230 assert_eq!(resp.requester_name, Some("Bob Leroy".to_string()));
1231 }
1232
1233 #[tokio::test]
1234 async fn test_start_exchange_success() {
1235 let owner_repo = Arc::new(MockOwnerRepository::new());
1236 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1237 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1238
1239 let provider_user_id = Uuid::new_v4();
1240 let requester_user_id = Uuid::new_v4();
1241 let building_id = Uuid::new_v4();
1242
1243 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1244 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1245 owner_repo.insert(provider.clone());
1246 owner_repo.insert(requester.clone());
1247
1248 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1249
1250 let dto = CreateLocalExchangeDto {
1252 building_id,
1253 exchange_type: ExchangeType::Service,
1254 title: "Plumbing fix".to_string(),
1255 description: "Fix leaking pipe".to_string(),
1256 credits: 2,
1257 };
1258 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1259 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1260 .await
1261 .unwrap();
1262
1263 let result = uc.start_exchange(created.id, provider_user_id).await;
1265 assert!(result.is_ok(), "start_exchange failed: {:?}", result.err());
1266
1267 let resp = result.unwrap();
1268 assert_eq!(resp.status, ExchangeStatus::InProgress);
1269 assert!(resp.started_at.is_some());
1270 }
1271
1272 #[tokio::test]
1273 async fn test_complete_exchange_updates_credit_balances() {
1274 let owner_repo = Arc::new(MockOwnerRepository::new());
1275 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1276 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1277
1278 let provider_user_id = Uuid::new_v4();
1279 let requester_user_id = Uuid::new_v4();
1280 let building_id = Uuid::new_v4();
1281
1282 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1283 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1284 owner_repo.insert(provider.clone());
1285 owner_repo.insert(requester.clone());
1286
1287 let uc = setup_use_cases(
1288 owner_repo.clone(),
1289 exchange_repo.clone(),
1290 balance_repo.clone(),
1291 );
1292
1293 let credits = 5;
1295 let dto = CreateLocalExchangeDto {
1296 building_id,
1297 exchange_type: ExchangeType::Service,
1298 title: "IT Help".to_string(),
1299 description: "Install software".to_string(),
1300 credits,
1301 };
1302 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1303 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1304 .await
1305 .unwrap();
1306 uc.start_exchange(created.id, provider_user_id)
1307 .await
1308 .unwrap();
1309
1310 let result = uc
1312 .complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1313 .await;
1314 assert!(
1315 result.is_ok(),
1316 "complete_exchange failed: {:?}",
1317 result.err()
1318 );
1319
1320 let resp = result.unwrap();
1321 assert_eq!(resp.status, ExchangeStatus::Completed);
1322 assert!(resp.completed_at.is_some());
1323
1324 let provider_balance = uc
1326 .get_credit_balance(provider.id, building_id)
1327 .await
1328 .unwrap();
1329 assert_eq!(provider_balance.credits_earned, credits);
1330 assert_eq!(provider_balance.balance, credits);
1331 assert_eq!(provider_balance.total_exchanges, 1);
1332
1333 let requester_balance = uc
1334 .get_credit_balance(requester.id, building_id)
1335 .await
1336 .unwrap();
1337 assert_eq!(requester_balance.credits_spent, credits);
1338 assert_eq!(requester_balance.balance, -credits);
1339 assert_eq!(requester_balance.total_exchanges, 1);
1340 }
1341
1342 #[tokio::test]
1343 async fn test_cancel_exchange_success() {
1344 let owner_repo = Arc::new(MockOwnerRepository::new());
1345 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1346 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1347
1348 let provider_user_id = Uuid::new_v4();
1349 let requester_user_id = Uuid::new_v4();
1350 let building_id = Uuid::new_v4();
1351
1352 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1353 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1354 owner_repo.insert(provider.clone());
1355 owner_repo.insert(requester.clone());
1356
1357 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1358
1359 let dto = CreateLocalExchangeDto {
1361 building_id,
1362 exchange_type: ExchangeType::SharedPurchase,
1363 title: "Bulk order".to_string(),
1364 description: "Shared cleaning supplies".to_string(),
1365 credits: 2,
1366 };
1367 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1368 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1369 .await
1370 .unwrap();
1371
1372 let cancel_dto = CancelExchangeDto {
1374 reason: Some("Changed my mind".to_string()),
1375 };
1376 let result = uc
1377 .cancel_exchange(created.id, requester_user_id, cancel_dto)
1378 .await;
1379 assert!(result.is_ok(), "cancel_exchange failed: {:?}", result.err());
1380
1381 let resp = result.unwrap();
1382 assert_eq!(resp.status, ExchangeStatus::Cancelled);
1383 assert!(resp.cancelled_at.is_some());
1384 assert_eq!(
1385 resp.cancellation_reason,
1386 Some("Changed my mind".to_string())
1387 );
1388 }
1389
1390 #[tokio::test]
1391 async fn test_rate_provider_success() {
1392 let owner_repo = Arc::new(MockOwnerRepository::new());
1393 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1394 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1395
1396 let provider_user_id = Uuid::new_v4();
1397 let requester_user_id = Uuid::new_v4();
1398 let building_id = Uuid::new_v4();
1399
1400 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1401 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1402 owner_repo.insert(provider.clone());
1403 owner_repo.insert(requester.clone());
1404
1405 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1406
1407 let dto = CreateLocalExchangeDto {
1409 building_id,
1410 exchange_type: ExchangeType::Service,
1411 title: "Painting".to_string(),
1412 description: "Paint the hallway".to_string(),
1413 credits: 4,
1414 };
1415 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1416 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1417 .await
1418 .unwrap();
1419 uc.start_exchange(created.id, provider_user_id)
1420 .await
1421 .unwrap();
1422 uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1423 .await
1424 .unwrap();
1425
1426 let rate_dto = RateExchangeDto { rating: 5 };
1428 let result = uc
1429 .rate_provider(created.id, requester_user_id, rate_dto)
1430 .await;
1431 assert!(result.is_ok(), "rate_provider failed: {:?}", result.err());
1432
1433 let resp = result.unwrap();
1434 assert_eq!(resp.provider_rating, Some(5));
1435
1436 let provider_balance = balance_repo
1438 .find_by_owner_and_building(provider.id, building_id)
1439 .await
1440 .unwrap()
1441 .unwrap();
1442 assert_eq!(provider_balance.average_rating, Some(5.0));
1443 }
1444
1445 #[tokio::test]
1446 async fn test_rate_requester_success() {
1447 let owner_repo = Arc::new(MockOwnerRepository::new());
1448 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1449 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1450
1451 let provider_user_id = Uuid::new_v4();
1452 let requester_user_id = Uuid::new_v4();
1453 let building_id = Uuid::new_v4();
1454
1455 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1456 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1457 owner_repo.insert(provider.clone());
1458 owner_repo.insert(requester.clone());
1459
1460 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1461
1462 let dto = CreateLocalExchangeDto {
1464 building_id,
1465 exchange_type: ExchangeType::Service,
1466 title: "Babysitting".to_string(),
1467 description: "Watch kids for 3 hours".to_string(),
1468 credits: 3,
1469 };
1470 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1471 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1472 .await
1473 .unwrap();
1474 uc.start_exchange(created.id, provider_user_id)
1475 .await
1476 .unwrap();
1477 uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1478 .await
1479 .unwrap();
1480
1481 let rate_dto = RateExchangeDto { rating: 4 };
1483 let result = uc
1484 .rate_requester(created.id, provider_user_id, rate_dto)
1485 .await;
1486 assert!(result.is_ok(), "rate_requester failed: {:?}", result.err());
1487
1488 let resp = result.unwrap();
1489 assert_eq!(resp.requester_rating, Some(4));
1490
1491 let requester_balance = balance_repo
1493 .find_by_owner_and_building(requester.id, building_id)
1494 .await
1495 .unwrap()
1496 .unwrap();
1497 assert_eq!(requester_balance.average_rating, Some(4.0));
1498 }
1499
1500 #[tokio::test]
1501 async fn test_get_credit_balance_creates_if_missing() {
1502 let owner_repo = Arc::new(MockOwnerRepository::new());
1503 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1504 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1505
1506 let user_id = Uuid::new_v4();
1507 let owner = make_owner(user_id);
1508 let owner_id = owner.id;
1509 let building_id = Uuid::new_v4();
1510 owner_repo.insert(owner);
1511
1512 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1513
1514 let result = uc.get_credit_balance(owner_id, building_id).await;
1516 assert!(
1517 result.is_ok(),
1518 "get_credit_balance failed: {:?}",
1519 result.err()
1520 );
1521
1522 let balance = result.unwrap();
1523 assert_eq!(balance.owner_id, owner_id);
1524 assert_eq!(balance.building_id, building_id);
1525 assert_eq!(balance.credits_earned, 0);
1526 assert_eq!(balance.credits_spent, 0);
1527 assert_eq!(balance.balance, 0);
1528 assert_eq!(balance.total_exchanges, 0);
1529 }
1530}