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> {
40 let owner = self
41 .owner_repo
42 .find_by_user_id(user_id)
43 .await?
44 .ok_or_else(|| {
45 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES.to_string()
46 })?;
47 Ok(owner.id)
48 }
49
50 pub async fn create_exchange(
52 &self,
53 user_id: Uuid, dto: CreateLocalExchangeDto,
55 ) -> Result<LocalExchangeResponseDto, String> {
56 let provider = self
73 .owner_repo
74 .find_by_user_id(user_id)
75 .await?
76 .ok_or_else(|| {
77 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES.to_string()
78 })?;
79
80 let exchange = LocalExchange::new(
82 dto.building_id,
83 provider.id,
84 dto.exchange_type,
85 dto.title,
86 dto.description,
87 dto.credits,
88 )?;
89
90 let created = self.exchange_repo.create(&exchange).await?;
92
93 Ok(LocalExchangeResponseDto::from_entity(
95 created,
96 format!("{} {}", provider.first_name, provider.last_name),
97 None,
98 ))
99 }
100
101 pub async fn get_exchange(&self, id: Uuid) -> Result<LocalExchangeResponseDto, String> {
103 let exchange = self
104 .exchange_repo
105 .find_by_id(id)
106 .await?
107 .ok_or("Exchange not found".to_string())?;
108
109 let provider = self
117 .owner_repo
118 .find_by_id(exchange.provider_id)
119 .await?
120 .ok_or_else(|| {
121 format!(
122 "L'auteur de cet échange est introuvable (copropriétaire {}). \
123 La fiche a peut-être été supprimée ou anonymisée.",
124 exchange.provider_id
125 )
126 })?;
127 let provider_name = format!("{} {}", provider.first_name, provider.last_name);
128
129 let requester_name = if let Some(requester_id) = exchange.requester_id {
131 let requester = self.owner_repo.find_by_id(requester_id).await?;
132 requester.map(|r| format!("{} {}", r.first_name, r.last_name))
133 } else {
134 None
135 };
136
137 Ok(LocalExchangeResponseDto::from_entity(
138 exchange,
139 provider_name,
140 requester_name,
141 ))
142 }
143
144 pub async fn list_building_exchanges(
146 &self,
147 building_id: Uuid,
148 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
149 let exchanges = self.exchange_repo.find_by_building(building_id).await?;
150
151 self.enrich_exchanges_with_names(exchanges).await
152 }
153
154 pub async fn list_available_exchanges(
156 &self,
157 building_id: Uuid,
158 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
159 let exchanges = self
160 .exchange_repo
161 .find_available_by_building(building_id)
162 .await?;
163
164 self.enrich_exchanges_with_names(exchanges).await
165 }
166
167 pub async fn list_owner_exchanges(
169 &self,
170 owner_id: Uuid,
171 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
172 let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
173
174 self.enrich_exchanges_with_names(exchanges).await
175 }
176
177 pub async fn list_exchanges_by_type(
179 &self,
180 building_id: Uuid,
181 exchange_type: ExchangeType,
182 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
183 let exchanges = self
184 .exchange_repo
185 .find_by_type(building_id, exchange_type.to_sql())
186 .await?;
187
188 self.enrich_exchanges_with_names(exchanges).await
189 }
190
191 pub async fn request_exchange(
193 &self,
194 exchange_id: Uuid,
195 user_id: Uuid, _dto: RequestExchangeDto,
197 ) -> Result<LocalExchangeResponseDto, String> {
198 let owner_id = self.resolve_owner_id(user_id).await?;
199
200 let mut exchange = self
202 .exchange_repo
203 .find_by_id(exchange_id)
204 .await?
205 .ok_or("Exchange not found".to_string())?;
206
207 exchange.request(owner_id)?;
209
210 let updated = self.exchange_repo.update(&exchange).await?;
212
213 self.get_exchange(updated.id).await
215 }
216
217 pub async fn start_exchange(
220 &self,
221 exchange_id: Uuid,
222 user_id: Uuid, ) -> Result<LocalExchangeResponseDto, String> {
224 let owner_id = self.resolve_owner_id(user_id).await?;
225
226 let mut exchange = self
227 .exchange_repo
228 .find_by_id(exchange_id)
229 .await?
230 .ok_or("Exchange not found".to_string())?;
231
232 exchange.start(owner_id)?;
233
234 let updated = self.exchange_repo.update(&exchange).await?;
235
236 self.get_exchange(updated.id).await
237 }
238
239 pub async fn complete_exchange(
242 &self,
243 exchange_id: Uuid,
244 user_id: Uuid, _dto: CompleteExchangeDto,
246 ) -> Result<LocalExchangeResponseDto, String> {
247 let owner_id = self.resolve_owner_id(user_id).await?;
248
249 let mut exchange = self
250 .exchange_repo
251 .find_by_id(exchange_id)
252 .await?
253 .ok_or("Exchange not found".to_string())?;
254
255 let requester_id = exchange
257 .requester_id
258 .ok_or("Exchange has no requester".to_string())?;
259
260 exchange.complete(owner_id)?;
261
262 let updated = self.exchange_repo.update(&exchange).await?;
263
264 self.update_credit_balances_on_completion(&updated).await?;
266
267 let mut provider_balance = self
269 .balance_repo
270 .get_or_create(updated.provider_id, updated.building_id)
271 .await?;
272 provider_balance.increment_exchanges();
273 self.balance_repo.update(&provider_balance).await?;
274
275 let mut requester_balance = self
276 .balance_repo
277 .get_or_create(requester_id, updated.building_id)
278 .await?;
279 requester_balance.increment_exchanges();
280 self.balance_repo.update(&requester_balance).await?;
281
282 self.get_exchange(updated.id).await
283 }
284
285 pub async fn cancel_exchange(
298 &self,
299 exchange_id: Uuid,
300 user_id: Uuid, actor_role: &str,
302 dto: CancelExchangeDto,
303 ) -> Result<LocalExchangeResponseDto, String> {
304 let owner_id = self
305 .owner_repo
306 .find_by_user_id(user_id)
307 .await?
308 .map(|o| o.id);
309
310 let mut exchange = self
311 .exchange_repo
312 .find_by_id(exchange_id)
313 .await?
314 .ok_or("Exchange not found".to_string())?;
315
316 let party_owner_id = owner_id
317 .filter(|oid| exchange.provider_id == *oid || exchange.requester_id == Some(*oid));
318
319 match party_owner_id {
320 Some(oid) => {
321 exchange.cancel(oid, dto.reason)?;
322 }
323 None if crate::application::community_permissions::peut_moderer(actor_role) => {
324 let reason = dto.reason.filter(|r| !r.trim().is_empty()).ok_or_else(|| {
325 crate::application::error::MOTIF_MODERATION_REQUIS.to_string()
326 })?;
327 exchange.moderate_cancel(reason)?;
328 }
329 None => {
330 return Err(
331 "Only the provider, requester, or a community moderator can cancel the exchange"
332 .to_string(),
333 );
334 }
335 }
336
337 let updated = self.exchange_repo.update(&exchange).await?;
338
339 self.get_exchange(updated.id).await
340 }
341
342 pub async fn rate_provider(
344 &self,
345 exchange_id: Uuid,
346 user_id: Uuid, dto: RateExchangeDto,
348 ) -> Result<LocalExchangeResponseDto, String> {
349 let owner_id = self.resolve_owner_id(user_id).await?;
350
351 let mut exchange = self
352 .exchange_repo
353 .find_by_id(exchange_id)
354 .await?
355 .ok_or("Exchange not found".to_string())?;
356
357 exchange.rate_provider(owner_id, dto.rating)?;
358
359 let updated = self.exchange_repo.update(&exchange).await?;
360
361 self.update_average_rating(updated.provider_id, updated.building_id)
363 .await?;
364
365 self.get_exchange(updated.id).await
366 }
367
368 pub async fn rate_requester(
370 &self,
371 exchange_id: Uuid,
372 user_id: Uuid, dto: RateExchangeDto,
374 ) -> Result<LocalExchangeResponseDto, String> {
375 let owner_id = self.resolve_owner_id(user_id).await?;
376
377 let mut exchange = self
378 .exchange_repo
379 .find_by_id(exchange_id)
380 .await?
381 .ok_or("Exchange not found".to_string())?;
382
383 exchange.rate_requester(owner_id, dto.rating)?;
384
385 let updated = self.exchange_repo.update(&exchange).await?;
386
387 if let Some(requester_id) = updated.requester_id {
389 self.update_average_rating(requester_id, updated.building_id)
390 .await?;
391 }
392
393 self.get_exchange(updated.id).await
394 }
395
396 pub async fn delete_exchange(
404 &self,
405 exchange_id: Uuid,
406 user_id: Uuid,
407 actor_role: &str,
408 reason: Option<String>,
409 ) -> Result<(), String> {
410 let owner_id = self
411 .owner_repo
412 .find_by_user_id(user_id)
413 .await?
414 .map(|o| o.id);
415
416 let exchange = self
417 .exchange_repo
418 .find_by_id(exchange_id)
419 .await?
420 .ok_or("Exchange not found".to_string())?;
421
422 let is_provider = owner_id
423 .map(|oid| exchange.provider_id == oid)
424 .unwrap_or(false);
425
426 if !is_provider {
427 if crate::application::community_permissions::peut_moderer(actor_role) {
428 reason.filter(|r| !r.trim().is_empty()).ok_or_else(|| {
429 crate::application::error::MOTIF_MODERATION_REQUIS.to_string()
430 })?;
431 } else {
432 return Err(
433 "Only the provider or a community moderator can delete the exchange"
434 .to_string(),
435 );
436 }
437 }
438
439 if exchange.status == ExchangeStatus::Completed {
441 return Err("Cannot delete a completed exchange".to_string());
442 }
443
444 self.exchange_repo.delete(exchange_id).await?;
445
446 Ok(())
447 }
448
449 pub async fn get_credit_balance(
451 &self,
452 owner_id: Uuid,
453 building_id: Uuid,
454 ) -> Result<OwnerCreditBalanceDto, String> {
455 let balance = self
456 .balance_repo
457 .get_or_create(owner_id, building_id)
458 .await?;
459
460 let owner = self
461 .owner_repo
462 .find_by_id(owner_id)
463 .await?
464 .ok_or("Owner not found".to_string())?;
465 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
466
467 Ok(OwnerCreditBalanceDto::from_entity(balance, owner_name))
468 }
469
470 pub async fn get_leaderboard(
472 &self,
473 building_id: Uuid,
474 limit: i32,
475 ) -> Result<Vec<OwnerCreditBalanceDto>, String> {
476 let balances = self
477 .balance_repo
478 .get_leaderboard(building_id, limit)
479 .await?;
480
481 let mut dtos = Vec::new();
482 for balance in balances {
483 if let Some(owner) = self.owner_repo.find_by_id(balance.owner_id).await? {
484 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
485 dtos.push(OwnerCreditBalanceDto::from_entity(balance, owner_name));
486 }
487 }
488
489 Ok(dtos)
490 }
491
492 pub async fn get_statistics(&self, building_id: Uuid) -> Result<SelStatisticsDto, String> {
494 let total_exchanges = self.exchange_repo.count_by_building(building_id).await? as i32;
495
496 let active_exchanges = self
497 .exchange_repo
498 .count_by_building_and_status(building_id, ExchangeStatus::Offered.to_sql())
499 .await? as i32
500 + self
501 .exchange_repo
502 .count_by_building_and_status(building_id, ExchangeStatus::Requested.to_sql())
503 .await? as i32
504 + self
505 .exchange_repo
506 .count_by_building_and_status(building_id, ExchangeStatus::InProgress.to_sql())
507 .await? as i32;
508
509 let completed_exchanges = self
510 .exchange_repo
511 .count_by_building_and_status(building_id, ExchangeStatus::Completed.to_sql())
512 .await? as i32;
513
514 let total_credits_exchanged = self
515 .exchange_repo
516 .get_total_credits_exchanged(building_id)
517 .await?;
518
519 let active_participants = self
520 .balance_repo
521 .count_active_participants(building_id)
522 .await? as i32;
523
524 let average_exchange_rating = self
525 .exchange_repo
526 .get_average_exchange_rating(building_id)
527 .await?;
528
529 let service_count = self
531 .exchange_repo
532 .count_by_building_and_type(building_id, ExchangeType::Service.to_sql())
533 .await?;
534 let object_loan_count = self
535 .exchange_repo
536 .count_by_building_and_type(building_id, ExchangeType::ObjectLoan.to_sql())
537 .await?;
538 let shared_purchase_count = self
539 .exchange_repo
540 .count_by_building_and_type(building_id, ExchangeType::SharedPurchase.to_sql())
541 .await?;
542
543 let most_popular_exchange_type =
544 if service_count >= object_loan_count && service_count >= shared_purchase_count {
545 Some(ExchangeType::Service)
546 } else if object_loan_count >= shared_purchase_count {
547 Some(ExchangeType::ObjectLoan)
548 } else {
549 Some(ExchangeType::SharedPurchase)
550 };
551
552 Ok(SelStatisticsDto {
553 building_id,
554 total_exchanges,
555 active_exchanges,
556 completed_exchanges,
557 total_credits_exchanged,
558 active_participants,
559 average_exchange_rating,
560 most_popular_exchange_type,
561 })
562 }
563
564 pub async fn get_owner_summary(
566 &self,
567 owner_id: Uuid,
568 ) -> Result<OwnerExchangeSummaryDto, String> {
569 let owner = self
570 .owner_repo
571 .find_by_id(owner_id)
572 .await?
573 .ok_or("Owner not found".to_string())?;
574 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
575
576 let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
577
578 let as_provider = exchanges
579 .iter()
580 .filter(|e| e.provider_id == owner_id)
581 .count() as i32;
582
583 let as_requester = exchanges
584 .iter()
585 .filter(|e| e.requester_id == Some(owner_id))
586 .count() as i32;
587
588 let mut total_ratings = 0;
590 let mut rating_count = 0;
591
592 for exchange in &exchanges {
593 if exchange.provider_id == owner_id {
594 if let Some(rating) = exchange.provider_rating {
595 total_ratings += rating;
596 rating_count += 1;
597 }
598 }
599 if exchange.requester_id == Some(owner_id) {
600 if let Some(rating) = exchange.requester_rating {
601 total_ratings += rating;
602 rating_count += 1;
603 }
604 }
605 }
606
607 let average_rating = if rating_count > 0 {
608 Some(total_ratings as f32 / rating_count as f32)
609 } else {
610 None
611 };
612
613 let recent: Vec<_> = exchanges.into_iter().take(5).collect();
615 let recent_dtos = self.enrich_exchanges_with_names(recent).await?;
616
617 Ok(OwnerExchangeSummaryDto {
618 owner_id,
619 owner_name,
620 as_provider,
621 as_requester,
622 total_exchanges: as_provider + as_requester,
623 average_rating,
624 recent_exchanges: recent_dtos,
625 })
626 }
627
628 async fn update_credit_balances_on_completion(
632 &self,
633 exchange: &LocalExchange,
634 ) -> Result<(), String> {
635 let requester_id = exchange
636 .requester_id
637 .ok_or("Exchange has no requester".to_string())?;
638
639 let mut provider_balance = self
641 .balance_repo
642 .get_or_create(exchange.provider_id, exchange.building_id)
643 .await?;
644 provider_balance.earn_credits(exchange.credits)?;
645 self.balance_repo.update(&provider_balance).await?;
646
647 let mut requester_balance = self
649 .balance_repo
650 .get_or_create(requester_id, exchange.building_id)
651 .await?;
652 requester_balance.spend_credits(exchange.credits)?;
653 self.balance_repo.update(&requester_balance).await?;
654
655 Ok(())
656 }
657
658 async fn update_average_rating(&self, owner_id: Uuid, building_id: Uuid) -> Result<(), String> {
660 let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
661
662 let mut total_ratings = 0;
663 let mut rating_count = 0;
664
665 for exchange in exchanges {
666 if exchange.provider_id == owner_id {
667 if let Some(rating) = exchange.provider_rating {
668 total_ratings += rating;
669 rating_count += 1;
670 }
671 }
672 if exchange.requester_id == Some(owner_id) {
673 if let Some(rating) = exchange.requester_rating {
674 total_ratings += rating;
675 rating_count += 1;
676 }
677 }
678 }
679
680 if rating_count > 0 {
681 let average = total_ratings as f32 / rating_count as f32;
682
683 let mut balance = self
684 .balance_repo
685 .get_or_create(owner_id, building_id)
686 .await?;
687 balance.update_rating(average)?;
688 self.balance_repo.update(&balance).await?;
689 }
690
691 Ok(())
692 }
693
694 async fn enrich_exchanges_with_names(
696 &self,
697 exchanges: Vec<LocalExchange>,
698 ) -> Result<Vec<LocalExchangeResponseDto>, String> {
699 let mut dtos = Vec::new();
700
701 for exchange in exchanges {
702 let provider = self.owner_repo.find_by_id(exchange.provider_id).await?;
704 let provider_name = if let Some(p) = provider {
705 format!("{} {}", p.first_name, p.last_name)
706 } else {
707 "Unknown".to_string()
708 };
709
710 let requester_name = if let Some(requester_id) = exchange.requester_id {
712 let requester = self.owner_repo.find_by_id(requester_id).await?;
713 requester.map(|r| format!("{} {}", r.first_name, r.last_name))
714 } else {
715 None
716 };
717
718 dtos.push(LocalExchangeResponseDto::from_entity(
719 exchange,
720 provider_name,
721 requester_name,
722 ));
723 }
724
725 Ok(dtos)
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use crate::application::dto::{OwnerFilters, PageRequest};
733 use crate::application::ports::{
734 LocalExchangeRepository, OwnerCreditBalanceRepository, OwnerRepository,
735 };
736 use crate::domain::entities::{
737 ExchangeStatus, ExchangeType, LocalExchange, Owner, OwnerCreditBalance,
738 };
739 use async_trait::async_trait;
740 use chrono::Utc;
741 use std::collections::HashMap;
742 use std::sync::Mutex;
743
744 struct MockOwnerRepository {
747 owners: Mutex<HashMap<Uuid, Owner>>,
748 }
749
750 impl MockOwnerRepository {
751 fn new() -> Self {
752 Self {
753 owners: Mutex::new(HashMap::new()),
754 }
755 }
756
757 fn insert(&self, owner: Owner) {
758 self.owners.lock().unwrap().insert(owner.id, owner);
759 }
760 }
761
762 #[async_trait]
763 impl OwnerRepository for MockOwnerRepository {
764 async fn create(&self, owner: &Owner) -> Result<Owner, String> {
765 self.owners.lock().unwrap().insert(owner.id, owner.clone());
766 Ok(owner.clone())
767 }
768
769 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
770 Ok(self.owners.lock().unwrap().get(&id).cloned())
771 }
772
773 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
774 Ok(self
775 .owners
776 .lock()
777 .unwrap()
778 .values()
779 .find(|o| o.user_id == Some(user_id))
780 .cloned())
781 }
782
783 async fn find_by_user_id_and_organization(
784 &self,
785 user_id: Uuid,
786 organization_id: Uuid,
787 ) -> Result<Option<Owner>, String> {
788 Ok(self
789 .owners
790 .lock()
791 .unwrap()
792 .values()
793 .find(|o| o.user_id == Some(user_id) && o.organization_id == organization_id)
794 .cloned())
795 }
796
797 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
798 Ok(self
799 .owners
800 .lock()
801 .unwrap()
802 .values()
803 .find(|o| o.email == email)
804 .cloned())
805 }
806
807 async fn find_all(&self) -> Result<Vec<Owner>, String> {
808 Ok(self.owners.lock().unwrap().values().cloned().collect())
809 }
810
811 async fn find_all_paginated(
812 &self,
813 _page_request: &PageRequest,
814 _filters: &OwnerFilters,
815 ) -> Result<(Vec<Owner>, i64), String> {
816 let owners: Vec<_> = self.owners.lock().unwrap().values().cloned().collect();
817 let total = owners.len() as i64;
818 Ok((owners, total))
819 }
820
821 async fn update(&self, owner: &Owner) -> Result<Owner, String> {
822 self.owners.lock().unwrap().insert(owner.id, owner.clone());
823 Ok(owner.clone())
824 }
825
826 async fn delete(&self, id: Uuid) -> Result<bool, String> {
827 Ok(self.owners.lock().unwrap().remove(&id).is_some())
828 }
829 async fn set_user_link(
830 &self,
831 owner_id: Uuid,
832 user_id: Option<Uuid>,
833 ) -> Result<bool, String> {
834 let mut map = self.owners.lock().unwrap();
835 if let Some(o) = map.get_mut(&owner_id) {
836 o.user_id = user_id;
837 Ok(true)
838 } else {
839 Ok(false)
840 }
841 }
842 }
843
844 struct MockLocalExchangeRepository {
847 exchanges: Mutex<HashMap<Uuid, LocalExchange>>,
848 }
849
850 impl MockLocalExchangeRepository {
851 fn new() -> Self {
852 Self {
853 exchanges: Mutex::new(HashMap::new()),
854 }
855 }
856 }
857
858 #[async_trait]
859 impl LocalExchangeRepository for MockLocalExchangeRepository {
860 async fn create(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
861 self.exchanges
862 .lock()
863 .unwrap()
864 .insert(exchange.id, exchange.clone());
865 Ok(exchange.clone())
866 }
867
868 async fn find_by_id(&self, id: Uuid) -> Result<Option<LocalExchange>, String> {
869 Ok(self.exchanges.lock().unwrap().get(&id).cloned())
870 }
871
872 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<LocalExchange>, String> {
873 Ok(self
874 .exchanges
875 .lock()
876 .unwrap()
877 .values()
878 .filter(|e| e.building_id == building_id)
879 .cloned()
880 .collect())
881 }
882
883 async fn find_by_building_and_status(
884 &self,
885 building_id: Uuid,
886 status: &str,
887 ) -> Result<Vec<LocalExchange>, String> {
888 Ok(self
889 .exchanges
890 .lock()
891 .unwrap()
892 .values()
893 .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
894 .cloned()
895 .collect())
896 }
897
898 async fn find_by_provider(&self, provider_id: Uuid) -> Result<Vec<LocalExchange>, String> {
899 Ok(self
900 .exchanges
901 .lock()
902 .unwrap()
903 .values()
904 .filter(|e| e.provider_id == provider_id)
905 .cloned()
906 .collect())
907 }
908
909 async fn find_by_requester(
910 &self,
911 requester_id: Uuid,
912 ) -> Result<Vec<LocalExchange>, String> {
913 Ok(self
914 .exchanges
915 .lock()
916 .unwrap()
917 .values()
918 .filter(|e| e.requester_id == Some(requester_id))
919 .cloned()
920 .collect())
921 }
922
923 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<LocalExchange>, String> {
924 Ok(self
925 .exchanges
926 .lock()
927 .unwrap()
928 .values()
929 .filter(|e| e.provider_id == owner_id || e.requester_id == Some(owner_id))
930 .cloned()
931 .collect())
932 }
933
934 async fn find_active_by_building(
935 &self,
936 building_id: Uuid,
937 ) -> Result<Vec<LocalExchange>, String> {
938 Ok(self
939 .exchanges
940 .lock()
941 .unwrap()
942 .values()
943 .filter(|e| e.building_id == building_id && e.is_active())
944 .cloned()
945 .collect())
946 }
947
948 async fn find_available_by_building(
949 &self,
950 building_id: Uuid,
951 ) -> Result<Vec<LocalExchange>, String> {
952 Ok(self
953 .exchanges
954 .lock()
955 .unwrap()
956 .values()
957 .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Offered)
958 .cloned()
959 .collect())
960 }
961
962 async fn find_by_type(
963 &self,
964 building_id: Uuid,
965 exchange_type: &str,
966 ) -> Result<Vec<LocalExchange>, String> {
967 Ok(self
968 .exchanges
969 .lock()
970 .unwrap()
971 .values()
972 .filter(|e| {
973 e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
974 })
975 .cloned()
976 .collect())
977 }
978
979 async fn update(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
980 self.exchanges
981 .lock()
982 .unwrap()
983 .insert(exchange.id, exchange.clone());
984 Ok(exchange.clone())
985 }
986
987 async fn delete(&self, id: Uuid) -> Result<bool, String> {
988 Ok(self.exchanges.lock().unwrap().remove(&id).is_some())
989 }
990
991 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
992 Ok(self
993 .exchanges
994 .lock()
995 .unwrap()
996 .values()
997 .filter(|e| e.building_id == building_id)
998 .count() as i64)
999 }
1000
1001 async fn count_by_building_and_status(
1002 &self,
1003 building_id: Uuid,
1004 status: &str,
1005 ) -> Result<i64, String> {
1006 Ok(self
1007 .exchanges
1008 .lock()
1009 .unwrap()
1010 .values()
1011 .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
1012 .count() as i64)
1013 }
1014
1015 async fn count_by_building_and_type(
1016 &self,
1017 building_id: Uuid,
1018 exchange_type: &str,
1019 ) -> Result<i64, String> {
1020 Ok(self
1021 .exchanges
1022 .lock()
1023 .unwrap()
1024 .values()
1025 .filter(|e| {
1026 e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
1027 })
1028 .count() as i64)
1029 }
1030
1031 async fn get_total_credits_exchanged(&self, building_id: Uuid) -> Result<i32, String> {
1032 Ok(self
1033 .exchanges
1034 .lock()
1035 .unwrap()
1036 .values()
1037 .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Completed)
1038 .map(|e| e.credits)
1039 .sum())
1040 }
1041
1042 async fn get_average_exchange_rating(
1043 &self,
1044 building_id: Uuid,
1045 ) -> Result<Option<f32>, String> {
1046 let ratings: Vec<f32> = self
1047 .exchanges
1048 .lock()
1049 .unwrap()
1050 .values()
1051 .filter(|e| e.building_id == building_id)
1052 .flat_map(|e| {
1053 let mut v = Vec::new();
1054 if let Some(r) = e.provider_rating {
1055 v.push(r as f32);
1056 }
1057 if let Some(r) = e.requester_rating {
1058 v.push(r as f32);
1059 }
1060 v
1061 })
1062 .collect();
1063 if ratings.is_empty() {
1064 Ok(None)
1065 } else {
1066 Ok(Some(ratings.iter().sum::<f32>() / ratings.len() as f32))
1067 }
1068 }
1069 }
1070
1071 struct MockOwnerCreditBalanceRepository {
1074 balances: Mutex<HashMap<(Uuid, Uuid), OwnerCreditBalance>>,
1075 }
1076
1077 impl MockOwnerCreditBalanceRepository {
1078 fn new() -> Self {
1079 Self {
1080 balances: Mutex::new(HashMap::new()),
1081 }
1082 }
1083 }
1084
1085 #[async_trait]
1086 impl OwnerCreditBalanceRepository for MockOwnerCreditBalanceRepository {
1087 async fn create(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
1088 self.balances
1089 .lock()
1090 .unwrap()
1091 .insert((balance.owner_id, balance.building_id), balance.clone());
1092 Ok(balance.clone())
1093 }
1094
1095 async fn find_by_owner_and_building(
1096 &self,
1097 owner_id: Uuid,
1098 building_id: Uuid,
1099 ) -> Result<Option<OwnerCreditBalance>, String> {
1100 Ok(self
1101 .balances
1102 .lock()
1103 .unwrap()
1104 .get(&(owner_id, building_id))
1105 .cloned())
1106 }
1107
1108 async fn find_by_building(
1109 &self,
1110 building_id: Uuid,
1111 ) -> Result<Vec<OwnerCreditBalance>, String> {
1112 Ok(self
1113 .balances
1114 .lock()
1115 .unwrap()
1116 .values()
1117 .filter(|b| b.building_id == building_id)
1118 .cloned()
1119 .collect())
1120 }
1121
1122 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerCreditBalance>, String> {
1123 Ok(self
1124 .balances
1125 .lock()
1126 .unwrap()
1127 .values()
1128 .filter(|b| b.owner_id == owner_id)
1129 .cloned()
1130 .collect())
1131 }
1132
1133 async fn get_or_create(
1134 &self,
1135 owner_id: Uuid,
1136 building_id: Uuid,
1137 ) -> Result<OwnerCreditBalance, String> {
1138 let mut map = self.balances.lock().unwrap();
1139 if let Some(existing) = map.get(&(owner_id, building_id)) {
1140 Ok(existing.clone())
1141 } else {
1142 let balance = OwnerCreditBalance::new(owner_id, building_id);
1143 map.insert((owner_id, building_id), balance.clone());
1144 Ok(balance)
1145 }
1146 }
1147
1148 async fn update(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
1149 self.balances
1150 .lock()
1151 .unwrap()
1152 .insert((balance.owner_id, balance.building_id), balance.clone());
1153 Ok(balance.clone())
1154 }
1155
1156 async fn delete(&self, owner_id: Uuid, building_id: Uuid) -> Result<bool, String> {
1157 Ok(self
1158 .balances
1159 .lock()
1160 .unwrap()
1161 .remove(&(owner_id, building_id))
1162 .is_some())
1163 }
1164
1165 async fn get_leaderboard(
1166 &self,
1167 building_id: Uuid,
1168 limit: i32,
1169 ) -> Result<Vec<OwnerCreditBalance>, String> {
1170 let mut balances: Vec<_> = self
1171 .balances
1172 .lock()
1173 .unwrap()
1174 .values()
1175 .filter(|b| b.building_id == building_id)
1176 .cloned()
1177 .collect();
1178 balances.sort_by_key(|a| std::cmp::Reverse(a.balance));
1179 balances.truncate(limit as usize);
1180 Ok(balances)
1181 }
1182
1183 async fn count_active_participants(&self, building_id: Uuid) -> Result<i64, String> {
1184 Ok(self
1185 .balances
1186 .lock()
1187 .unwrap()
1188 .values()
1189 .filter(|b| b.building_id == building_id && b.total_exchanges > 0)
1190 .count() as i64)
1191 }
1192
1193 async fn get_total_credits_in_circulation(&self, building_id: Uuid) -> Result<i32, String> {
1194 Ok(self
1195 .balances
1196 .lock()
1197 .unwrap()
1198 .values()
1199 .filter(|b| b.building_id == building_id)
1200 .map(|b| b.credits_earned)
1201 .sum())
1202 }
1203 }
1204
1205 fn make_owner(user_id: Uuid) -> Owner {
1208 let now = Utc::now();
1209 Owner {
1210 id: Uuid::new_v4(),
1211 organization_id: Uuid::new_v4(),
1212 user_id: Some(user_id),
1213 first_name: "Jean".to_string(),
1214 last_name: "Dupont".to_string(),
1215 email: "jean@example.com".to_string(),
1216 phone: None,
1217 address: "1 rue de la Loi".to_string(),
1218 city: "Bruxelles".to_string(),
1219 postal_code: "1000".to_string(),
1220 country: "BE".to_string(),
1221 created_at: now,
1222 updated_at: now,
1223 }
1224 }
1225
1226 fn make_owner_with_name(user_id: Uuid, first: &str, last: &str) -> Owner {
1227 let now = Utc::now();
1228 Owner {
1229 id: Uuid::new_v4(),
1230 organization_id: Uuid::new_v4(),
1231 user_id: Some(user_id),
1232 first_name: first.to_string(),
1233 last_name: last.to_string(),
1234 email: format!("{}@example.com", first.to_lowercase()),
1235 phone: None,
1236 address: "1 rue de la Loi".to_string(),
1237 city: "Bruxelles".to_string(),
1238 postal_code: "1000".to_string(),
1239 country: "BE".to_string(),
1240 created_at: now,
1241 updated_at: now,
1242 }
1243 }
1244
1245 fn setup_use_cases(
1246 owner_repo: Arc<MockOwnerRepository>,
1247 exchange_repo: Arc<MockLocalExchangeRepository>,
1248 balance_repo: Arc<MockOwnerCreditBalanceRepository>,
1249 ) -> LocalExchangeUseCases {
1250 LocalExchangeUseCases::new(exchange_repo, balance_repo, owner_repo)
1251 }
1252
1253 #[tokio::test]
1256 async fn test_create_exchange_success() {
1257 let owner_repo = Arc::new(MockOwnerRepository::new());
1258 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1259 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1260
1261 let user_id = Uuid::new_v4();
1262 let provider = make_owner_with_name(user_id, "Alice", "Martin");
1263 owner_repo.insert(provider.clone());
1264
1265 let building_id = Uuid::new_v4();
1266 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1267
1268 let dto = CreateLocalExchangeDto {
1269 building_id,
1270 exchange_type: ExchangeType::Service,
1271 title: "Gardening help".to_string(),
1272 description: "I can help with your garden".to_string(),
1273 credits: 3,
1274 };
1275
1276 let result = uc.create_exchange(user_id, dto).await;
1277 assert!(result.is_ok(), "create_exchange failed: {:?}", result.err());
1278
1279 let resp = result.unwrap();
1280 assert_eq!(resp.building_id, building_id);
1281 assert_eq!(resp.provider_id, provider.id);
1282 assert_eq!(resp.provider_name, "Alice Martin");
1283 assert_eq!(resp.status, ExchangeStatus::Offered);
1284 assert_eq!(resp.credits, 3);
1285 assert_eq!(resp.title, "Gardening help");
1286 assert!(resp.requester_id.is_none());
1287 }
1288
1289 #[tokio::test]
1290 async fn test_request_exchange_success() {
1291 let owner_repo = Arc::new(MockOwnerRepository::new());
1292 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1293 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1294
1295 let provider_user_id = Uuid::new_v4();
1296 let requester_user_id = Uuid::new_v4();
1297 let building_id = Uuid::new_v4();
1298
1299 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1300 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1301 owner_repo.insert(provider.clone());
1302 owner_repo.insert(requester.clone());
1303
1304 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1305
1306 let dto = CreateLocalExchangeDto {
1308 building_id,
1309 exchange_type: ExchangeType::ObjectLoan,
1310 title: "Drill to lend".to_string(),
1311 description: "Heavy-duty drill available".to_string(),
1312 credits: 1,
1313 };
1314 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1315
1316 let result = uc
1318 .request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1319 .await;
1320 assert!(
1321 result.is_ok(),
1322 "request_exchange failed: {:?}",
1323 result.err()
1324 );
1325
1326 let resp = result.unwrap();
1327 assert_eq!(resp.status, ExchangeStatus::Requested);
1328 assert_eq!(resp.requester_id, Some(requester.id));
1329 assert_eq!(resp.requester_name, Some("Bob Leroy".to_string()));
1330 }
1331
1332 #[tokio::test]
1333 async fn test_start_exchange_success() {
1334 let owner_repo = Arc::new(MockOwnerRepository::new());
1335 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1336 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1337
1338 let provider_user_id = Uuid::new_v4();
1339 let requester_user_id = Uuid::new_v4();
1340 let building_id = Uuid::new_v4();
1341
1342 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1343 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1344 owner_repo.insert(provider.clone());
1345 owner_repo.insert(requester.clone());
1346
1347 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1348
1349 let dto = CreateLocalExchangeDto {
1351 building_id,
1352 exchange_type: ExchangeType::Service,
1353 title: "Plumbing fix".to_string(),
1354 description: "Fix leaking pipe".to_string(),
1355 credits: 2,
1356 };
1357 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1358 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1359 .await
1360 .unwrap();
1361
1362 let result = uc.start_exchange(created.id, provider_user_id).await;
1364 assert!(result.is_ok(), "start_exchange failed: {:?}", result.err());
1365
1366 let resp = result.unwrap();
1367 assert_eq!(resp.status, ExchangeStatus::InProgress);
1368 assert!(resp.started_at.is_some());
1369 }
1370
1371 #[tokio::test]
1372 async fn test_complete_exchange_updates_credit_balances() {
1373 let owner_repo = Arc::new(MockOwnerRepository::new());
1374 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1375 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1376
1377 let provider_user_id = Uuid::new_v4();
1378 let requester_user_id = Uuid::new_v4();
1379 let building_id = Uuid::new_v4();
1380
1381 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1382 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1383 owner_repo.insert(provider.clone());
1384 owner_repo.insert(requester.clone());
1385
1386 let uc = setup_use_cases(
1387 owner_repo.clone(),
1388 exchange_repo.clone(),
1389 balance_repo.clone(),
1390 );
1391
1392 let credits = 5;
1394 let dto = CreateLocalExchangeDto {
1395 building_id,
1396 exchange_type: ExchangeType::Service,
1397 title: "IT Help".to_string(),
1398 description: "Install software".to_string(),
1399 credits,
1400 };
1401 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1402 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1403 .await
1404 .unwrap();
1405 uc.start_exchange(created.id, provider_user_id)
1406 .await
1407 .unwrap();
1408
1409 let result = uc
1411 .complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1412 .await;
1413 assert!(
1414 result.is_ok(),
1415 "complete_exchange failed: {:?}",
1416 result.err()
1417 );
1418
1419 let resp = result.unwrap();
1420 assert_eq!(resp.status, ExchangeStatus::Completed);
1421 assert!(resp.completed_at.is_some());
1422
1423 let provider_balance = uc
1425 .get_credit_balance(provider.id, building_id)
1426 .await
1427 .unwrap();
1428 assert_eq!(provider_balance.credits_earned, credits);
1429 assert_eq!(provider_balance.balance, credits);
1430 assert_eq!(provider_balance.total_exchanges, 1);
1431
1432 let requester_balance = uc
1433 .get_credit_balance(requester.id, building_id)
1434 .await
1435 .unwrap();
1436 assert_eq!(requester_balance.credits_spent, credits);
1437 assert_eq!(requester_balance.balance, -credits);
1438 assert_eq!(requester_balance.total_exchanges, 1);
1439 }
1440
1441 #[tokio::test]
1442 async fn test_cancel_exchange_success() {
1443 let owner_repo = Arc::new(MockOwnerRepository::new());
1444 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1445 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1446
1447 let provider_user_id = Uuid::new_v4();
1448 let requester_user_id = Uuid::new_v4();
1449 let building_id = Uuid::new_v4();
1450
1451 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1452 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1453 owner_repo.insert(provider.clone());
1454 owner_repo.insert(requester.clone());
1455
1456 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1457
1458 let dto = CreateLocalExchangeDto {
1460 building_id,
1461 exchange_type: ExchangeType::SharedPurchase,
1462 title: "Bulk order".to_string(),
1463 description: "Shared cleaning supplies".to_string(),
1464 credits: 2,
1465 };
1466 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1467 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1468 .await
1469 .unwrap();
1470
1471 let cancel_dto = CancelExchangeDto {
1473 reason: Some("Changed my mind".to_string()),
1474 };
1475 let result = uc
1476 .cancel_exchange(created.id, requester_user_id, "owner", cancel_dto)
1477 .await;
1478 assert!(result.is_ok(), "cancel_exchange failed: {:?}", result.err());
1479
1480 let resp = result.unwrap();
1481 assert_eq!(resp.status, ExchangeStatus::Cancelled);
1482 assert!(resp.cancelled_at.is_some());
1483 assert_eq!(
1484 resp.cancellation_reason,
1485 Some("Changed my mind".to_string())
1486 );
1487 }
1488
1489 #[tokio::test]
1494 async fn security_syndic_pur_ne_peut_pas_creer_une_offre_sel() {
1495 let owner_repo = Arc::new(MockOwnerRepository::new());
1499 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1500 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1501 let syndic_user_id = Uuid::new_v4();
1502 let building_id = Uuid::new_v4();
1503
1504 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1505
1506 let dto = CreateLocalExchangeDto {
1507 building_id,
1508 exchange_type: ExchangeType::Service,
1509 title: "Coup de main".to_string(),
1510 description: "Offre personnelle".to_string(),
1511 credits: 1,
1512 };
1513
1514 let result = uc.create_exchange(syndic_user_id, dto).await;
1515 assert!(result.is_err());
1516 assert_eq!(
1517 result.unwrap_err(),
1518 crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES
1519 );
1520 }
1521
1522 #[tokio::test]
1523 async fn happy_syndic_moderator_annule_un_echange_litigieux_avec_motif() {
1524 let owner_repo = Arc::new(MockOwnerRepository::new());
1525 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1526 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1527
1528 let provider_user_id = Uuid::new_v4();
1529 let syndic_user_id = Uuid::new_v4(); let building_id = Uuid::new_v4();
1531
1532 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1533 owner_repo.insert(provider.clone());
1534
1535 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1536
1537 let dto = CreateLocalExchangeDto {
1538 building_id,
1539 exchange_type: ExchangeType::Service,
1540 title: "Bricolage".to_string(),
1541 description: "Offre litigieuse".to_string(),
1542 credits: 3,
1543 };
1544 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1545
1546 let cancel_dto = CancelExchangeDto {
1547 reason: Some("Signalement d'un voisin : offre inappropriée".to_string()),
1548 };
1549 let result = uc
1550 .cancel_exchange(created.id, syndic_user_id, "syndic", cancel_dto)
1551 .await;
1552 assert!(
1553 result.is_ok(),
1554 "un syndic modérateur doit pouvoir annuler un échange litigieux : {:?}",
1555 result.err()
1556 );
1557 assert_eq!(result.unwrap().status, ExchangeStatus::Cancelled);
1558 }
1559
1560 #[tokio::test]
1561 async fn negative_moderation_sans_motif_est_refusee() {
1562 let owner_repo = Arc::new(MockOwnerRepository::new());
1563 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1564 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1565
1566 let provider_user_id = Uuid::new_v4();
1567 let syndic_user_id = Uuid::new_v4();
1568 let building_id = Uuid::new_v4();
1569
1570 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1571 owner_repo.insert(provider.clone());
1572
1573 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1574
1575 let dto = CreateLocalExchangeDto {
1576 building_id,
1577 exchange_type: ExchangeType::Service,
1578 title: "Bricolage".to_string(),
1579 description: "Offre".to_string(),
1580 credits: 3,
1581 };
1582 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1583
1584 let cancel_dto = CancelExchangeDto { reason: None };
1586 let result = uc
1587 .cancel_exchange(created.id, syndic_user_id, "syndic", cancel_dto)
1588 .await;
1589 assert!(result.is_err());
1590 assert_eq!(
1591 result.unwrap_err(),
1592 crate::application::error::MOTIF_MODERATION_REQUIS
1593 );
1594
1595 let cancel_dto_blank = CancelExchangeDto {
1597 reason: Some(" ".to_string()),
1598 };
1599 let result_blank = uc
1600 .cancel_exchange(created.id, syndic_user_id, "syndic", cancel_dto_blank)
1601 .await;
1602 assert!(result_blank.is_err());
1603 assert_eq!(
1604 result_blank.unwrap_err(),
1605 crate::application::error::MOTIF_MODERATION_REQUIS
1606 );
1607 }
1608
1609 #[tokio::test]
1610 async fn security_ni_partie_ni_moderateur_ne_peut_annuler() {
1611 let owner_repo = Arc::new(MockOwnerRepository::new());
1612 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1613 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1614
1615 let provider_user_id = Uuid::new_v4();
1616 let bystander_user_id = Uuid::new_v4(); let building_id = Uuid::new_v4();
1618
1619 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1620 let bystander = make_owner_with_name(bystander_user_id, "Carl", "Voisin");
1621 owner_repo.insert(provider.clone());
1622 owner_repo.insert(bystander.clone());
1623
1624 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1625
1626 let dto = CreateLocalExchangeDto {
1627 building_id,
1628 exchange_type: ExchangeType::Service,
1629 title: "Bricolage".to_string(),
1630 description: "Offre".to_string(),
1631 credits: 3,
1632 };
1633 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1634
1635 let cancel_dto = CancelExchangeDto {
1636 reason: Some("Je n'aime pas cette offre".to_string()),
1637 };
1638 let result = uc
1639 .cancel_exchange(created.id, bystander_user_id, "owner", cancel_dto)
1640 .await;
1641 assert!(result.is_err());
1642 assert!(result
1643 .unwrap_err()
1644 .contains("Only the provider, requester, or a community moderator"));
1645 }
1646
1647 #[tokio::test]
1648 async fn edge_syndic_cumulant_le_role_owner_annule_son_propre_echange_sans_motif() {
1649 let owner_repo = Arc::new(MockOwnerRepository::new());
1654 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1655 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1656
1657 let syndic_owner_user_id = Uuid::new_v4();
1658 let building_id = Uuid::new_v4();
1659
1660 let syndic_as_owner = make_owner_with_name(syndic_owner_user_id, "Denise", "Syndic");
1661 owner_repo.insert(syndic_as_owner.clone());
1662
1663 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1664
1665 let dto = CreateLocalExchangeDto {
1666 building_id,
1667 exchange_type: ExchangeType::Service,
1668 title: "Offre du syndic-copropriétaire".to_string(),
1669 description: "Offre personnelle, ès qualités de copropriétaire".to_string(),
1670 credits: 1,
1671 };
1672 let created = uc.create_exchange(syndic_owner_user_id, dto).await.expect(
1673 "un syndic qui a un lot doit pouvoir créer une offre ès qualités de copropriétaire",
1674 );
1675
1676 let cancel_dto = CancelExchangeDto { reason: None };
1677 let result = uc
1678 .cancel_exchange(created.id, syndic_owner_user_id, "syndic", cancel_dto)
1679 .await;
1680 assert!(
1681 result.is_ok(),
1682 "le provider n'a pas à motiver l'annulation de sa propre offre : {:?}",
1683 result.err()
1684 );
1685 assert_eq!(result.unwrap().status, ExchangeStatus::Cancelled);
1686 }
1687
1688 #[tokio::test]
1689 async fn happy_syndic_moderator_supprime_un_echange_litigieux_avec_motif() {
1690 let owner_repo = Arc::new(MockOwnerRepository::new());
1691 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1692 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1693
1694 let provider_user_id = Uuid::new_v4();
1695 let syndic_user_id = Uuid::new_v4();
1696 let building_id = Uuid::new_v4();
1697
1698 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1699 owner_repo.insert(provider.clone());
1700
1701 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1702
1703 let dto = CreateLocalExchangeDto {
1704 building_id,
1705 exchange_type: ExchangeType::Service,
1706 title: "Bricolage".to_string(),
1707 description: "Offre litigieuse".to_string(),
1708 credits: 3,
1709 };
1710 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1711
1712 let result = uc
1713 .delete_exchange(
1714 created.id,
1715 syndic_user_id,
1716 "syndic",
1717 Some("Contenu inapproprié signalé".to_string()),
1718 )
1719 .await;
1720 assert!(
1721 result.is_ok(),
1722 "un syndic modérateur doit pouvoir supprimer une offre litigieuse : {:?}",
1723 result.err()
1724 );
1725 assert!(uc.get_exchange(created.id).await.is_err());
1726 }
1727
1728 #[tokio::test]
1729 async fn negative_suppression_par_moderation_sans_motif_est_refusee() {
1730 let owner_repo = Arc::new(MockOwnerRepository::new());
1731 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1732 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1733
1734 let provider_user_id = Uuid::new_v4();
1735 let syndic_user_id = Uuid::new_v4();
1736 let building_id = Uuid::new_v4();
1737
1738 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1739 owner_repo.insert(provider.clone());
1740
1741 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1742
1743 let dto = CreateLocalExchangeDto {
1744 building_id,
1745 exchange_type: ExchangeType::Service,
1746 title: "Bricolage".to_string(),
1747 description: "Offre".to_string(),
1748 credits: 3,
1749 };
1750 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1751
1752 let result = uc
1753 .delete_exchange(created.id, syndic_user_id, "syndic", None)
1754 .await;
1755 assert!(result.is_err());
1756 assert_eq!(
1757 result.unwrap_err(),
1758 crate::application::error::MOTIF_MODERATION_REQUIS
1759 );
1760 }
1761
1762 #[tokio::test]
1763 async fn security_ni_provider_ni_moderateur_ne_peut_supprimer() {
1764 let owner_repo = Arc::new(MockOwnerRepository::new());
1765 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1766 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1767
1768 let provider_user_id = Uuid::new_v4();
1769 let bystander_user_id = Uuid::new_v4();
1770 let building_id = Uuid::new_v4();
1771
1772 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1773 let bystander = make_owner_with_name(bystander_user_id, "Carl", "Voisin");
1774 owner_repo.insert(provider.clone());
1775 owner_repo.insert(bystander.clone());
1776
1777 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1778
1779 let dto = CreateLocalExchangeDto {
1780 building_id,
1781 exchange_type: ExchangeType::Service,
1782 title: "Bricolage".to_string(),
1783 description: "Offre".to_string(),
1784 credits: 3,
1785 };
1786 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1787
1788 let result = uc
1789 .delete_exchange(created.id, bystander_user_id, "owner", None)
1790 .await;
1791 assert!(result.is_err());
1792 assert!(result
1793 .unwrap_err()
1794 .contains("Only the provider or a community moderator"));
1795 }
1796
1797 #[tokio::test]
1798 async fn test_rate_provider_success() {
1799 let owner_repo = Arc::new(MockOwnerRepository::new());
1800 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1801 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1802
1803 let provider_user_id = Uuid::new_v4();
1804 let requester_user_id = Uuid::new_v4();
1805 let building_id = Uuid::new_v4();
1806
1807 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1808 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1809 owner_repo.insert(provider.clone());
1810 owner_repo.insert(requester.clone());
1811
1812 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1813
1814 let dto = CreateLocalExchangeDto {
1816 building_id,
1817 exchange_type: ExchangeType::Service,
1818 title: "Painting".to_string(),
1819 description: "Paint the hallway".to_string(),
1820 credits: 4,
1821 };
1822 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1823 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1824 .await
1825 .unwrap();
1826 uc.start_exchange(created.id, provider_user_id)
1827 .await
1828 .unwrap();
1829 uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1830 .await
1831 .unwrap();
1832
1833 let rate_dto = RateExchangeDto { rating: 5 };
1835 let result = uc
1836 .rate_provider(created.id, requester_user_id, rate_dto)
1837 .await;
1838 assert!(result.is_ok(), "rate_provider failed: {:?}", result.err());
1839
1840 let resp = result.unwrap();
1841 assert_eq!(resp.provider_rating, Some(5));
1842
1843 let provider_balance = balance_repo
1845 .find_by_owner_and_building(provider.id, building_id)
1846 .await
1847 .unwrap()
1848 .unwrap();
1849 assert_eq!(provider_balance.average_rating, Some(5.0));
1850 }
1851
1852 #[tokio::test]
1853 async fn test_rate_requester_success() {
1854 let owner_repo = Arc::new(MockOwnerRepository::new());
1855 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1856 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1857
1858 let provider_user_id = Uuid::new_v4();
1859 let requester_user_id = Uuid::new_v4();
1860 let building_id = Uuid::new_v4();
1861
1862 let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1863 let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1864 owner_repo.insert(provider.clone());
1865 owner_repo.insert(requester.clone());
1866
1867 let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1868
1869 let dto = CreateLocalExchangeDto {
1871 building_id,
1872 exchange_type: ExchangeType::Service,
1873 title: "Babysitting".to_string(),
1874 description: "Watch kids for 3 hours".to_string(),
1875 credits: 3,
1876 };
1877 let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1878 uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1879 .await
1880 .unwrap();
1881 uc.start_exchange(created.id, provider_user_id)
1882 .await
1883 .unwrap();
1884 uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1885 .await
1886 .unwrap();
1887
1888 let rate_dto = RateExchangeDto { rating: 4 };
1890 let result = uc
1891 .rate_requester(created.id, provider_user_id, rate_dto)
1892 .await;
1893 assert!(result.is_ok(), "rate_requester failed: {:?}", result.err());
1894
1895 let resp = result.unwrap();
1896 assert_eq!(resp.requester_rating, Some(4));
1897
1898 let requester_balance = balance_repo
1900 .find_by_owner_and_building(requester.id, building_id)
1901 .await
1902 .unwrap()
1903 .unwrap();
1904 assert_eq!(requester_balance.average_rating, Some(4.0));
1905 }
1906
1907 #[tokio::test]
1908 async fn test_get_credit_balance_creates_if_missing() {
1909 let owner_repo = Arc::new(MockOwnerRepository::new());
1910 let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1911 let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1912
1913 let user_id = Uuid::new_v4();
1914 let owner = make_owner(user_id);
1915 let owner_id = owner.id;
1916 let building_id = Uuid::new_v4();
1917 owner_repo.insert(owner);
1918
1919 let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1920
1921 let result = uc.get_credit_balance(owner_id, building_id).await;
1923 assert!(
1924 result.is_ok(),
1925 "get_credit_balance failed: {:?}",
1926 result.err()
1927 );
1928
1929 let balance = result.unwrap();
1930 assert_eq!(balance.owner_id, owner_id);
1931 assert_eq!(balance.building_id, building_id);
1932 assert_eq!(balance.credits_earned, 0);
1933 assert_eq!(balance.credits_spent, 0);
1934 assert_eq!(balance.balance, 0);
1935 assert_eq!(balance.total_exchanges, 0);
1936 }
1937}