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 }
731
732 struct MockLocalExchangeRepository {
735 exchanges: Mutex<HashMap<Uuid, LocalExchange>>,
736 }
737
738 impl MockLocalExchangeRepository {
739 fn new() -> Self {
740 Self {
741 exchanges: Mutex::new(HashMap::new()),
742 }
743 }
744 }
745
746 #[async_trait]
747 impl LocalExchangeRepository for MockLocalExchangeRepository {
748 async fn create(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
749 self.exchanges
750 .lock()
751 .unwrap()
752 .insert(exchange.id, exchange.clone());
753 Ok(exchange.clone())
754 }
755
756 async fn find_by_id(&self, id: Uuid) -> Result<Option<LocalExchange>, String> {
757 Ok(self.exchanges.lock().unwrap().get(&id).cloned())
758 }
759
760 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<LocalExchange>, String> {
761 Ok(self
762 .exchanges
763 .lock()
764 .unwrap()
765 .values()
766 .filter(|e| e.building_id == building_id)
767 .cloned()
768 .collect())
769 }
770
771 async fn find_by_building_and_status(
772 &self,
773 building_id: Uuid,
774 status: &str,
775 ) -> Result<Vec<LocalExchange>, String> {
776 Ok(self
777 .exchanges
778 .lock()
779 .unwrap()
780 .values()
781 .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
782 .cloned()
783 .collect())
784 }
785
786 async fn find_by_provider(&self, provider_id: Uuid) -> Result<Vec<LocalExchange>, String> {
787 Ok(self
788 .exchanges
789 .lock()
790 .unwrap()
791 .values()
792 .filter(|e| e.provider_id == provider_id)
793 .cloned()
794 .collect())
795 }
796
797 async fn find_by_requester(
798 &self,
799 requester_id: Uuid,
800 ) -> Result<Vec<LocalExchange>, String> {
801 Ok(self
802 .exchanges
803 .lock()
804 .unwrap()
805 .values()
806 .filter(|e| e.requester_id == Some(requester_id))
807 .cloned()
808 .collect())
809 }
810
811 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<LocalExchange>, String> {
812 Ok(self
813 .exchanges
814 .lock()
815 .unwrap()
816 .values()
817 .filter(|e| e.provider_id == owner_id || e.requester_id == Some(owner_id))
818 .cloned()
819 .collect())
820 }
821
822 async fn find_active_by_building(
823 &self,
824 building_id: Uuid,
825 ) -> Result<Vec<LocalExchange>, String> {
826 Ok(self
827 .exchanges
828 .lock()
829 .unwrap()
830 .values()
831 .filter(|e| e.building_id == building_id && e.is_active())
832 .cloned()
833 .collect())
834 }
835
836 async fn find_available_by_building(
837 &self,
838 building_id: Uuid,
839 ) -> Result<Vec<LocalExchange>, String> {
840 Ok(self
841 .exchanges
842 .lock()
843 .unwrap()
844 .values()
845 .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Offered)
846 .cloned()
847 .collect())
848 }
849
850 async fn find_by_type(
851 &self,
852 building_id: Uuid,
853 exchange_type: &str,
854 ) -> Result<Vec<LocalExchange>, String> {
855 Ok(self
856 .exchanges
857 .lock()
858 .unwrap()
859 .values()
860 .filter(|e| {
861 e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
862 })
863 .cloned()
864 .collect())
865 }
866
867 async fn update(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
868 self.exchanges
869 .lock()
870 .unwrap()
871 .insert(exchange.id, exchange.clone());
872 Ok(exchange.clone())
873 }
874
875 async fn delete(&self, id: Uuid) -> Result<bool, String> {
876 Ok(self.exchanges.lock().unwrap().remove(&id).is_some())
877 }
878
879 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
880 Ok(self
881 .exchanges
882 .lock()
883 .unwrap()
884 .values()
885 .filter(|e| e.building_id == building_id)
886 .count() as i64)
887 }
888
889 async fn count_by_building_and_status(
890 &self,
891 building_id: Uuid,
892 status: &str,
893 ) -> Result<i64, String> {
894 Ok(self
895 .exchanges
896 .lock()
897 .unwrap()
898 .values()
899 .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
900 .count() as i64)
901 }
902
903 async fn count_by_building_and_type(
904 &self,
905 building_id: Uuid,
906 exchange_type: &str,
907 ) -> Result<i64, String> {
908 Ok(self
909 .exchanges
910 .lock()
911 .unwrap()
912 .values()
913 .filter(|e| {
914 e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
915 })
916 .count() as i64)
917 }
918
919 async fn get_total_credits_exchanged(&self, building_id: Uuid) -> Result<i32, String> {
920 Ok(self
921 .exchanges
922 .lock()
923 .unwrap()
924 .values()
925 .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Completed)
926 .map(|e| e.credits)
927 .sum())
928 }
929
930 async fn get_average_exchange_rating(
931 &self,
932 building_id: Uuid,
933 ) -> Result<Option<f32>, String> {
934 let ratings: Vec<f32> = self
935 .exchanges
936 .lock()
937 .unwrap()
938 .values()
939 .filter(|e| e.building_id == building_id)
940 .flat_map(|e| {
941 let mut v = Vec::new();
942 if let Some(r) = e.provider_rating {
943 v.push(r as f32);
944 }
945 if let Some(r) = e.requester_rating {
946 v.push(r as f32);
947 }
948 v
949 })
950 .collect();
951 if ratings.is_empty() {
952 Ok(None)
953 } else {
954 Ok(Some(ratings.iter().sum::<f32>() / ratings.len() as f32))
955 }
956 }
957 }
958
959 struct MockOwnerCreditBalanceRepository {
962 balances: Mutex<HashMap<(Uuid, Uuid), OwnerCreditBalance>>,
963 }
964
965 impl MockOwnerCreditBalanceRepository {
966 fn new() -> Self {
967 Self {
968 balances: Mutex::new(HashMap::new()),
969 }
970 }
971 }
972
973 #[async_trait]
974 impl OwnerCreditBalanceRepository for MockOwnerCreditBalanceRepository {
975 async fn create(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
976 self.balances
977 .lock()
978 .unwrap()
979 .insert((balance.owner_id, balance.building_id), balance.clone());
980 Ok(balance.clone())
981 }
982
983 async fn find_by_owner_and_building(
984 &self,
985 owner_id: Uuid,
986 building_id: Uuid,
987 ) -> Result<Option<OwnerCreditBalance>, String> {
988 Ok(self
989 .balances
990 .lock()
991 .unwrap()
992 .get(&(owner_id, building_id))
993 .cloned())
994 }
995
996 async fn find_by_building(
997 &self,
998 building_id: Uuid,
999 ) -> Result<Vec<OwnerCreditBalance>, String> {
1000 Ok(self
1001 .balances
1002 .lock()
1003 .unwrap()
1004 .values()
1005 .filter(|b| b.building_id == building_id)
1006 .cloned()
1007 .collect())
1008 }
1009
1010 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerCreditBalance>, String> {
1011 Ok(self
1012 .balances
1013 .lock()
1014 .unwrap()
1015 .values()
1016 .filter(|b| b.owner_id == owner_id)
1017 .cloned()
1018 .collect())
1019 }
1020
1021 async fn get_or_create(
1022 &self,
1023 owner_id: Uuid,
1024 building_id: Uuid,
1025 ) -> Result<OwnerCreditBalance, String> {
1026 let mut map = self.balances.lock().unwrap();
1027 if let Some(existing) = map.get(&(owner_id, building_id)) {
1028 Ok(existing.clone())
1029 } else {
1030 let balance = OwnerCreditBalance::new(owner_id, building_id);
1031 map.insert((owner_id, building_id), balance.clone());
1032 Ok(balance)
1033 }
1034 }
1035
1036 async fn update(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
1037 self.balances
1038 .lock()
1039 .unwrap()
1040 .insert((balance.owner_id, balance.building_id), balance.clone());
1041 Ok(balance.clone())
1042 }
1043
1044 async fn delete(&self, owner_id: Uuid, building_id: Uuid) -> Result<bool, String> {
1045 Ok(self
1046 .balances
1047 .lock()
1048 .unwrap()
1049 .remove(&(owner_id, building_id))
1050 .is_some())
1051 }
1052
1053 async fn get_leaderboard(
1054 &self,
1055 building_id: Uuid,
1056 limit: i32,
1057 ) -> Result<Vec<OwnerCreditBalance>, String> {
1058 let mut balances: Vec<_> = self
1059 .balances
1060 .lock()
1061 .unwrap()
1062 .values()
1063 .filter(|b| b.building_id == building_id)
1064 .cloned()
1065 .collect();
1066 balances.sort_by(|a, b| b.balance.cmp(&a.balance));
1067 balances.truncate(limit as usize);
1068 Ok(balances)
1069 }
1070
1071 async fn count_active_participants(&self, building_id: Uuid) -> Result<i64, String> {
1072 Ok(self
1073 .balances
1074 .lock()
1075 .unwrap()
1076 .values()
1077 .filter(|b| b.building_id == building_id && b.total_exchanges > 0)
1078 .count() as i64)
1079 }
1080
1081 async fn get_total_credits_in_circulation(&self, building_id: Uuid) -> Result<i32, String> {
1082 Ok(self
1083 .balances
1084 .lock()
1085 .unwrap()
1086 .values()
1087 .filter(|b| b.building_id == building_id)
1088 .map(|b| b.credits_earned)
1089 .sum())
1090 }
1091 }
1092
1093 fn make_owner(user_id: Uuid) -> Owner {
1096 let now = Utc::now();
1097 Owner {
1098 id: Uuid::new_v4(),
1099 organization_id: Uuid::new_v4(),
1100 user_id: Some(user_id),
1101 first_name: "Jean".to_string(),
1102 last_name: "Dupont".to_string(),
1103 email: "jean@example.com".to_string(),
1104 phone: None,
1105 address: "1 rue de la Loi".to_string(),
1106 city: "Bruxelles".to_string(),
1107 postal_code: "1000".to_string(),
1108 country: "BE".to_string(),
1109 created_at: now,
1110 updated_at: now,
1111 }
1112 }
1113
1114 fn make_owner_with_name(user_id: Uuid, first: &str, last: &str) -> Owner {
1115 let now = Utc::now();
1116 Owner {
1117 id: Uuid::new_v4(),
1118 organization_id: Uuid::new_v4(),
1119 user_id: Some(user_id),
1120 first_name: first.to_string(),
1121 last_name: last.to_string(),
1122 email: format!("{}@example.com", first.to_lowercase()),
1123 phone: None,
1124 address: "1 rue de la Loi".to_string(),
1125 city: "Bruxelles".to_string(),
1126 postal_code: "1000".to_string(),
1127 country: "BE".to_string(),
1128 created_at: now,
1129 updated_at: now,
1130 }
1131 }
1132
1133 fn setup_use_cases(
1134 owner_repo: Arc<MockOwnerRepository>,
1135 exchange_repo: Arc<MockLocalExchangeRepository>,
1136 balance_repo: Arc<MockOwnerCreditBalanceRepository>,
1137 ) -> LocalExchangeUseCases {
1138 LocalExchangeUseCases::new(exchange_repo, balance_repo, owner_repo)
1139 }
1140
1141 #[tokio::test]
1144 async fn test_create_exchange_success() {
1145 let owner_repo = Arc::new(MockOwnerRepository::new());
1146 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1147 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1148
1149 let user_id = Uuid::new_v4();
1150 let provider = make_owner_with_name(user_id, "Alice", "Martin");
1151 owner_repo.insert(provider.clone());
1152
1153 let building_id = Uuid::new_v4();
1154 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1155
1156 let dto = CreateLocalExchangeDto {
1157 building_id,
1158 exchange_type: ExchangeType::Service,
1159 title: "Gardening help".to_string(),
1160 description: "I can help with your garden".to_string(),
1161 credits: 3,
1162 };
1163
1164 let result = uc.create_exchange(user_id, dto).await;
1165 assert!(result.is_ok(), "create_exchange failed: {:?}", result.err());
1166
1167 let resp = result.unwrap();
1168 assert_eq!(resp.building_id, building_id);
1169 assert_eq!(resp.provider_id, provider.id);
1170 assert_eq!(resp.provider_name, "Alice Martin");
1171 assert_eq!(resp.status, ExchangeStatus::Offered);
1172 assert_eq!(resp.credits, 3);
1173 assert_eq!(resp.title, "Gardening help");
1174 assert!(resp.requester_id.is_none());
1175 }
1176
1177 #[tokio::test]
1178 async fn test_request_exchange_success() {
1179 let owner_repo = Arc::new(MockOwnerRepository::new());
1180 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1181 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1182
1183 let provider_user_id = Uuid::new_v4();
1184 let requester_user_id = Uuid::new_v4();
1185 let building_id = Uuid::new_v4();
1186
1187 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1188 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1189 owner_repo.insert(provider.clone());
1190 owner_repo.insert(requester.clone());
1191
1192 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1193
1194 let dto = CreateLocalExchangeDto {
1196 building_id,
1197 exchange_type: ExchangeType::ObjectLoan,
1198 title: "Drill to lend".to_string(),
1199 description: "Heavy-duty drill available".to_string(),
1200 credits: 1,
1201 };
1202 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1203
1204 let result = uc
1206 .request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1207 .await;
1208 assert!(
1209 result.is_ok(),
1210 "request_exchange failed: {:?}",
1211 result.err()
1212 );
1213
1214 let resp = result.unwrap();
1215 assert_eq!(resp.status, ExchangeStatus::Requested);
1216 assert_eq!(resp.requester_id, Some(requester.id));
1217 assert_eq!(resp.requester_name, Some("Bob Leroy".to_string()));
1218 }
1219
1220 #[tokio::test]
1221 async fn test_start_exchange_success() {
1222 let owner_repo = Arc::new(MockOwnerRepository::new());
1223 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1224 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1225
1226 let provider_user_id = Uuid::new_v4();
1227 let requester_user_id = Uuid::new_v4();
1228 let building_id = Uuid::new_v4();
1229
1230 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1231 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1232 owner_repo.insert(provider.clone());
1233 owner_repo.insert(requester.clone());
1234
1235 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1236
1237 let dto = CreateLocalExchangeDto {
1239 building_id,
1240 exchange_type: ExchangeType::Service,
1241 title: "Plumbing fix".to_string(),
1242 description: "Fix leaking pipe".to_string(),
1243 credits: 2,
1244 };
1245 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1246 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1247 .await
1248 .unwrap();
1249
1250 let result = uc.start_exchange(created.id, provider_user_id).await;
1252 assert!(result.is_ok(), "start_exchange failed: {:?}", result.err());
1253
1254 let resp = result.unwrap();
1255 assert_eq!(resp.status, ExchangeStatus::InProgress);
1256 assert!(resp.started_at.is_some());
1257 }
1258
1259 #[tokio::test]
1260 async fn test_complete_exchange_updates_credit_balances() {
1261 let owner_repo = Arc::new(MockOwnerRepository::new());
1262 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1263 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1264
1265 let provider_user_id = Uuid::new_v4();
1266 let requester_user_id = Uuid::new_v4();
1267 let building_id = Uuid::new_v4();
1268
1269 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1270 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1271 owner_repo.insert(provider.clone());
1272 owner_repo.insert(requester.clone());
1273
1274 let uc = setup_use_cases(
1275 owner_repo.clone(),
1276 exchange_repo.clone(),
1277 balance_repo.clone(),
1278 );
1279
1280 let credits = 5;
1282 let dto = CreateLocalExchangeDto {
1283 building_id,
1284 exchange_type: ExchangeType::Service,
1285 title: "IT Help".to_string(),
1286 description: "Install software".to_string(),
1287 credits,
1288 };
1289 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1290 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1291 .await
1292 .unwrap();
1293 uc.start_exchange(created.id, provider_user_id)
1294 .await
1295 .unwrap();
1296
1297 let result = uc
1299 .complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1300 .await;
1301 assert!(
1302 result.is_ok(),
1303 "complete_exchange failed: {:?}",
1304 result.err()
1305 );
1306
1307 let resp = result.unwrap();
1308 assert_eq!(resp.status, ExchangeStatus::Completed);
1309 assert!(resp.completed_at.is_some());
1310
1311 let provider_balance = uc
1313 .get_credit_balance(provider.id, building_id)
1314 .await
1315 .unwrap();
1316 assert_eq!(provider_balance.credits_earned, credits);
1317 assert_eq!(provider_balance.balance, credits);
1318 assert_eq!(provider_balance.total_exchanges, 1);
1319
1320 let requester_balance = uc
1321 .get_credit_balance(requester.id, building_id)
1322 .await
1323 .unwrap();
1324 assert_eq!(requester_balance.credits_spent, credits);
1325 assert_eq!(requester_balance.balance, -credits);
1326 assert_eq!(requester_balance.total_exchanges, 1);
1327 }
1328
1329 #[tokio::test]
1330 async fn test_cancel_exchange_success() {
1331 let owner_repo = Arc::new(MockOwnerRepository::new());
1332 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1333 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1334
1335 let provider_user_id = Uuid::new_v4();
1336 let requester_user_id = Uuid::new_v4();
1337 let building_id = Uuid::new_v4();
1338
1339 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1340 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1341 owner_repo.insert(provider.clone());
1342 owner_repo.insert(requester.clone());
1343
1344 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1345
1346 let dto = CreateLocalExchangeDto {
1348 building_id,
1349 exchange_type: ExchangeType::SharedPurchase,
1350 title: "Bulk order".to_string(),
1351 description: "Shared cleaning supplies".to_string(),
1352 credits: 2,
1353 };
1354 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1355 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1356 .await
1357 .unwrap();
1358
1359 let cancel_dto = CancelExchangeDto {
1361 reason: Some("Changed my mind".to_string()),
1362 };
1363 let result = uc
1364 .cancel_exchange(created.id, requester_user_id, cancel_dto)
1365 .await;
1366 assert!(result.is_ok(), "cancel_exchange failed: {:?}", result.err());
1367
1368 let resp = result.unwrap();
1369 assert_eq!(resp.status, ExchangeStatus::Cancelled);
1370 assert!(resp.cancelled_at.is_some());
1371 assert_eq!(
1372 resp.cancellation_reason,
1373 Some("Changed my mind".to_string())
1374 );
1375 }
1376
1377 #[tokio::test]
1378 async fn test_rate_provider_success() {
1379 let owner_repo = Arc::new(MockOwnerRepository::new());
1380 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1381 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1382
1383 let provider_user_id = Uuid::new_v4();
1384 let requester_user_id = Uuid::new_v4();
1385 let building_id = Uuid::new_v4();
1386
1387 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1388 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1389 owner_repo.insert(provider.clone());
1390 owner_repo.insert(requester.clone());
1391
1392 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1393
1394 let dto = CreateLocalExchangeDto {
1396 building_id,
1397 exchange_type: ExchangeType::Service,
1398 title: "Painting".to_string(),
1399 description: "Paint the hallway".to_string(),
1400 credits: 4,
1401 };
1402 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1403 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1404 .await
1405 .unwrap();
1406 uc.start_exchange(created.id, provider_user_id)
1407 .await
1408 .unwrap();
1409 uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1410 .await
1411 .unwrap();
1412
1413 let rate_dto = RateExchangeDto { rating: 5 };
1415 let result = uc
1416 .rate_provider(created.id, requester_user_id, rate_dto)
1417 .await;
1418 assert!(result.is_ok(), "rate_provider failed: {:?}", result.err());
1419
1420 let resp = result.unwrap();
1421 assert_eq!(resp.provider_rating, Some(5));
1422
1423 let provider_balance = balance_repo
1425 .find_by_owner_and_building(provider.id, building_id)
1426 .await
1427 .unwrap()
1428 .unwrap();
1429 assert_eq!(provider_balance.average_rating, Some(5.0));
1430 }
1431
1432 #[tokio::test]
1433 async fn test_rate_requester_success() {
1434 let owner_repo = Arc::new(MockOwnerRepository::new());
1435 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1436 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1437
1438 let provider_user_id = Uuid::new_v4();
1439 let requester_user_id = Uuid::new_v4();
1440 let building_id = Uuid::new_v4();
1441
1442 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1443 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1444 owner_repo.insert(provider.clone());
1445 owner_repo.insert(requester.clone());
1446
1447 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1448
1449 let dto = CreateLocalExchangeDto {
1451 building_id,
1452 exchange_type: ExchangeType::Service,
1453 title: "Babysitting".to_string(),
1454 description: "Watch kids for 3 hours".to_string(),
1455 credits: 3,
1456 };
1457 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1458 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1459 .await
1460 .unwrap();
1461 uc.start_exchange(created.id, provider_user_id)
1462 .await
1463 .unwrap();
1464 uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1465 .await
1466 .unwrap();
1467
1468 let rate_dto = RateExchangeDto { rating: 4 };
1470 let result = uc
1471 .rate_requester(created.id, provider_user_id, rate_dto)
1472 .await;
1473 assert!(result.is_ok(), "rate_requester failed: {:?}", result.err());
1474
1475 let resp = result.unwrap();
1476 assert_eq!(resp.requester_rating, Some(4));
1477
1478 let requester_balance = balance_repo
1480 .find_by_owner_and_building(requester.id, building_id)
1481 .await
1482 .unwrap()
1483 .unwrap();
1484 assert_eq!(requester_balance.average_rating, Some(4.0));
1485 }
1486
1487 #[tokio::test]
1488 async fn test_get_credit_balance_creates_if_missing() {
1489 let owner_repo = Arc::new(MockOwnerRepository::new());
1490 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1491 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1492
1493 let user_id = Uuid::new_v4();
1494 let owner = make_owner(user_id);
1495 let owner_id = owner.id;
1496 let building_id = Uuid::new_v4();
1497 owner_repo.insert(owner);
1498
1499 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1500
1501 let result = uc.get_credit_balance(owner_id, building_id).await;
1503 assert!(
1504 result.is_ok(),
1505 "get_credit_balance failed: {:?}",
1506 result.err()
1507 );
1508
1509 let balance = result.unwrap();
1510 assert_eq!(balance.owner_id, owner_id);
1511 assert_eq!(balance.building_id, building_id);
1512 assert_eq!(balance.credits_earned, 0);
1513 assert_eq!(balance.credits_spent, 0);
1514 assert_eq!(balance.balance, 0);
1515 assert_eq!(balance.total_exchanges, 0);
1516 }
1517}