koprogo_api/application/use_cases/
local_exchange_use_cases.rs

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
13/// Use cases for Local Exchange Trading System (SEL)
14pub 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    /// Resolve a user_id (from auth) to an owner_id (from owners table)
34    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    /// Create a new exchange offer
44    pub async fn create_exchange(
45        &self,
46        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
47        dto: CreateLocalExchangeDto,
48    ) -> Result<LocalExchangeResponseDto, String> {
49        // Resolve user_id → owner
50        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        // Create domain entity using owner's actual ID
57        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        // Persist
67        let created = self.exchange_repo.create(&exchange).await?;
68
69        // Return DTO with provider name
70        Ok(LocalExchangeResponseDto::from_entity(
71            created,
72            format!("{} {}", provider.first_name, provider.last_name),
73            None,
74        ))
75    }
76
77    /// Get exchange by ID
78    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        // Get provider name
86        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        // Get requester name if exists
94        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    /// List all exchanges for a building
109    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    /// List available exchanges (status = Offered)
119    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    /// List exchanges by owner (as provider OR requester)
132    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    /// List exchanges by type
142    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    /// Request an exchange (transition: Offered → Requested)
156    pub async fn request_exchange(
157        &self,
158        exchange_id: Uuid,
159        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
160        _dto: RequestExchangeDto,
161    ) -> Result<LocalExchangeResponseDto, String> {
162        let owner_id = self.resolve_owner_id(user_id).await?;
163
164        // Load exchange
165        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        // Apply business logic
172        exchange.request(owner_id)?;
173
174        // Persist
175        let updated = self.exchange_repo.update(&exchange).await?;
176
177        // Return enriched DTO
178        self.get_exchange(updated.id).await
179    }
180
181    /// Start an exchange (transition: Requested → InProgress)
182    /// Only provider can start
183    pub async fn start_exchange(
184        &self,
185        exchange_id: Uuid,
186        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
187    ) -> 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    /// Complete an exchange (transition: InProgress → Completed)
204    /// Updates credit balances for both parties
205    pub async fn complete_exchange(
206        &self,
207        exchange_id: Uuid,
208        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
209        _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        // Get requester before completing (we'll need it for balance update)
220        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        // Update credit balances
229        self.update_credit_balances_on_completion(&updated).await?;
230
231        // Increment exchange counters
232        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    /// Cancel an exchange
250    pub async fn cancel_exchange(
251        &self,
252        exchange_id: Uuid,
253        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
254        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    /// Rate the provider (by requester)
272    pub async fn rate_provider(
273        &self,
274        exchange_id: Uuid,
275        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
276        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        // Update provider's average rating
291        self.update_average_rating(updated.provider_id, updated.building_id)
292            .await?;
293
294        self.get_exchange(updated.id).await
295    }
296
297    /// Rate the requester (by provider)
298    pub async fn rate_requester(
299        &self,
300        exchange_id: Uuid,
301        user_id: Uuid, // From auth (user_id, resolved to owner_id internally)
302        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        // Update requester's average rating
317        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    /// Delete an exchange (only if not completed)
326    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        // Only provider can delete
336        if exchange.provider_id != owner_id {
337            return Err("Only the provider can delete the exchange".to_string());
338        }
339
340        // Cannot delete completed exchanges
341        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    /// Get credit balance for an owner in a building
351    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    /// Get leaderboard (top contributors)
372    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    /// Get SEL statistics for a building
394    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        // Determine most popular exchange type
431        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    /// Get owner exchange summary
466    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        // Calculate average rating (average of all ratings received)
490        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        // Get recent 5 exchanges
515        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    // Private helper methods
530
531    /// Update credit balances when exchange is completed
532    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        // Provider earns credits
541        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        // Requester spends credits
549        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    /// Update average rating for an owner
560    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    /// Enrich exchanges with owner names (provider + requester)
596    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            // Get provider name
604            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            // Get requester name if exists
612            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    // ── Mock OwnerRepository ────────────────────────────────────────────
646
647    struct MockOwnerRepository {
648        owners: Mutex<HashMap<Uuid, Owner>>,
649    }
650
651    impl MockOwnerRepository {
652        fn new() -> Self {
653            Self {
654                owners: Mutex::new(HashMap::new()),
655            }
656        }
657
658        fn insert(&self, owner: Owner) {
659            self.owners.lock().unwrap().insert(owner.id, owner);
660        }
661    }
662
663    #[async_trait]
664    impl OwnerRepository for MockOwnerRepository {
665        async fn create(&self, owner: &Owner) -> Result<Owner, String> {
666            self.owners.lock().unwrap().insert(owner.id, owner.clone());
667            Ok(owner.clone())
668        }
669
670        async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
671            Ok(self.owners.lock().unwrap().get(&id).cloned())
672        }
673
674        async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
675            Ok(self
676                .owners
677                .lock()
678                .unwrap()
679                .values()
680                .find(|o| o.user_id == Some(user_id))
681                .cloned())
682        }
683
684        async fn find_by_user_id_and_organization(
685            &self,
686            user_id: Uuid,
687            organization_id: Uuid,
688        ) -> Result<Option<Owner>, String> {
689            Ok(self
690                .owners
691                .lock()
692                .unwrap()
693                .values()
694                .find(|o| o.user_id == Some(user_id) && o.organization_id == organization_id)
695                .cloned())
696        }
697
698        async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
699            Ok(self
700                .owners
701                .lock()
702                .unwrap()
703                .values()
704                .find(|o| o.email == email)
705                .cloned())
706        }
707
708        async fn find_all(&self) -> Result<Vec<Owner>, String> {
709            Ok(self.owners.lock().unwrap().values().cloned().collect())
710        }
711
712        async fn find_all_paginated(
713            &self,
714            _page_request: &PageRequest,
715            _filters: &OwnerFilters,
716        ) -> Result<(Vec<Owner>, i64), String> {
717            let owners: Vec<_> = self.owners.lock().unwrap().values().cloned().collect();
718            let total = owners.len() as i64;
719            Ok((owners, total))
720        }
721
722        async fn update(&self, owner: &Owner) -> Result<Owner, String> {
723            self.owners.lock().unwrap().insert(owner.id, owner.clone());
724            Ok(owner.clone())
725        }
726
727        async fn delete(&self, id: Uuid) -> Result<bool, String> {
728            Ok(self.owners.lock().unwrap().remove(&id).is_some())
729        }
730        async fn set_user_link(
731            &self,
732            owner_id: Uuid,
733            user_id: Option<Uuid>,
734        ) -> Result<bool, String> {
735            let mut map = self.owners.lock().unwrap();
736            if let Some(o) = map.get_mut(&owner_id) {
737                o.user_id = user_id;
738                Ok(true)
739            } else {
740                Ok(false)
741            }
742        }
743    }
744
745    // ── Mock LocalExchangeRepository ────────────────────────────────────
746
747    struct MockLocalExchangeRepository {
748        exchanges: Mutex<HashMap<Uuid, LocalExchange>>,
749    }
750
751    impl MockLocalExchangeRepository {
752        fn new() -> Self {
753            Self {
754                exchanges: Mutex::new(HashMap::new()),
755            }
756        }
757    }
758
759    #[async_trait]
760    impl LocalExchangeRepository for MockLocalExchangeRepository {
761        async fn create(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
762            self.exchanges
763                .lock()
764                .unwrap()
765                .insert(exchange.id, exchange.clone());
766            Ok(exchange.clone())
767        }
768
769        async fn find_by_id(&self, id: Uuid) -> Result<Option<LocalExchange>, String> {
770            Ok(self.exchanges.lock().unwrap().get(&id).cloned())
771        }
772
773        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<LocalExchange>, String> {
774            Ok(self
775                .exchanges
776                .lock()
777                .unwrap()
778                .values()
779                .filter(|e| e.building_id == building_id)
780                .cloned()
781                .collect())
782        }
783
784        async fn find_by_building_and_status(
785            &self,
786            building_id: Uuid,
787            status: &str,
788        ) -> Result<Vec<LocalExchange>, String> {
789            Ok(self
790                .exchanges
791                .lock()
792                .unwrap()
793                .values()
794                .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
795                .cloned()
796                .collect())
797        }
798
799        async fn find_by_provider(&self, provider_id: Uuid) -> Result<Vec<LocalExchange>, String> {
800            Ok(self
801                .exchanges
802                .lock()
803                .unwrap()
804                .values()
805                .filter(|e| e.provider_id == provider_id)
806                .cloned()
807                .collect())
808        }
809
810        async fn find_by_requester(
811            &self,
812            requester_id: Uuid,
813        ) -> Result<Vec<LocalExchange>, String> {
814            Ok(self
815                .exchanges
816                .lock()
817                .unwrap()
818                .values()
819                .filter(|e| e.requester_id == Some(requester_id))
820                .cloned()
821                .collect())
822        }
823
824        async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<LocalExchange>, String> {
825            Ok(self
826                .exchanges
827                .lock()
828                .unwrap()
829                .values()
830                .filter(|e| e.provider_id == owner_id || e.requester_id == Some(owner_id))
831                .cloned()
832                .collect())
833        }
834
835        async fn find_active_by_building(
836            &self,
837            building_id: Uuid,
838        ) -> Result<Vec<LocalExchange>, String> {
839            Ok(self
840                .exchanges
841                .lock()
842                .unwrap()
843                .values()
844                .filter(|e| e.building_id == building_id && e.is_active())
845                .cloned()
846                .collect())
847        }
848
849        async fn find_available_by_building(
850            &self,
851            building_id: Uuid,
852        ) -> Result<Vec<LocalExchange>, String> {
853            Ok(self
854                .exchanges
855                .lock()
856                .unwrap()
857                .values()
858                .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Offered)
859                .cloned()
860                .collect())
861        }
862
863        async fn find_by_type(
864            &self,
865            building_id: Uuid,
866            exchange_type: &str,
867        ) -> Result<Vec<LocalExchange>, String> {
868            Ok(self
869                .exchanges
870                .lock()
871                .unwrap()
872                .values()
873                .filter(|e| {
874                    e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
875                })
876                .cloned()
877                .collect())
878        }
879
880        async fn update(&self, exchange: &LocalExchange) -> Result<LocalExchange, String> {
881            self.exchanges
882                .lock()
883                .unwrap()
884                .insert(exchange.id, exchange.clone());
885            Ok(exchange.clone())
886        }
887
888        async fn delete(&self, id: Uuid) -> Result<bool, String> {
889            Ok(self.exchanges.lock().unwrap().remove(&id).is_some())
890        }
891
892        async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
893            Ok(self
894                .exchanges
895                .lock()
896                .unwrap()
897                .values()
898                .filter(|e| e.building_id == building_id)
899                .count() as i64)
900        }
901
902        async fn count_by_building_and_status(
903            &self,
904            building_id: Uuid,
905            status: &str,
906        ) -> Result<i64, String> {
907            Ok(self
908                .exchanges
909                .lock()
910                .unwrap()
911                .values()
912                .filter(|e| e.building_id == building_id && e.status.to_sql() == status)
913                .count() as i64)
914        }
915
916        async fn count_by_building_and_type(
917            &self,
918            building_id: Uuid,
919            exchange_type: &str,
920        ) -> Result<i64, String> {
921            Ok(self
922                .exchanges
923                .lock()
924                .unwrap()
925                .values()
926                .filter(|e| {
927                    e.building_id == building_id && e.exchange_type.to_sql() == exchange_type
928                })
929                .count() as i64)
930        }
931
932        async fn get_total_credits_exchanged(&self, building_id: Uuid) -> Result<i32, String> {
933            Ok(self
934                .exchanges
935                .lock()
936                .unwrap()
937                .values()
938                .filter(|e| e.building_id == building_id && e.status == ExchangeStatus::Completed)
939                .map(|e| e.credits)
940                .sum())
941        }
942
943        async fn get_average_exchange_rating(
944            &self,
945            building_id: Uuid,
946        ) -> Result<Option<f32>, String> {
947            let ratings: Vec<f32> = self
948                .exchanges
949                .lock()
950                .unwrap()
951                .values()
952                .filter(|e| e.building_id == building_id)
953                .flat_map(|e| {
954                    let mut v = Vec::new();
955                    if let Some(r) = e.provider_rating {
956                        v.push(r as f32);
957                    }
958                    if let Some(r) = e.requester_rating {
959                        v.push(r as f32);
960                    }
961                    v
962                })
963                .collect();
964            if ratings.is_empty() {
965                Ok(None)
966            } else {
967                Ok(Some(ratings.iter().sum::<f32>() / ratings.len() as f32))
968            }
969        }
970    }
971
972    // ── Mock OwnerCreditBalanceRepository ────────────────────────────────
973
974    struct MockOwnerCreditBalanceRepository {
975        balances: Mutex<HashMap<(Uuid, Uuid), OwnerCreditBalance>>,
976    }
977
978    impl MockOwnerCreditBalanceRepository {
979        fn new() -> Self {
980            Self {
981                balances: Mutex::new(HashMap::new()),
982            }
983        }
984    }
985
986    #[async_trait]
987    impl OwnerCreditBalanceRepository for MockOwnerCreditBalanceRepository {
988        async fn create(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
989            self.balances
990                .lock()
991                .unwrap()
992                .insert((balance.owner_id, balance.building_id), balance.clone());
993            Ok(balance.clone())
994        }
995
996        async fn find_by_owner_and_building(
997            &self,
998            owner_id: Uuid,
999            building_id: Uuid,
1000        ) -> Result<Option<OwnerCreditBalance>, String> {
1001            Ok(self
1002                .balances
1003                .lock()
1004                .unwrap()
1005                .get(&(owner_id, building_id))
1006                .cloned())
1007        }
1008
1009        async fn find_by_building(
1010            &self,
1011            building_id: Uuid,
1012        ) -> Result<Vec<OwnerCreditBalance>, String> {
1013            Ok(self
1014                .balances
1015                .lock()
1016                .unwrap()
1017                .values()
1018                .filter(|b| b.building_id == building_id)
1019                .cloned()
1020                .collect())
1021        }
1022
1023        async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<OwnerCreditBalance>, String> {
1024            Ok(self
1025                .balances
1026                .lock()
1027                .unwrap()
1028                .values()
1029                .filter(|b| b.owner_id == owner_id)
1030                .cloned()
1031                .collect())
1032        }
1033
1034        async fn get_or_create(
1035            &self,
1036            owner_id: Uuid,
1037            building_id: Uuid,
1038        ) -> Result<OwnerCreditBalance, String> {
1039            let mut map = self.balances.lock().unwrap();
1040            if let Some(existing) = map.get(&(owner_id, building_id)) {
1041                Ok(existing.clone())
1042            } else {
1043                let balance = OwnerCreditBalance::new(owner_id, building_id);
1044                map.insert((owner_id, building_id), balance.clone());
1045                Ok(balance)
1046            }
1047        }
1048
1049        async fn update(&self, balance: &OwnerCreditBalance) -> Result<OwnerCreditBalance, String> {
1050            self.balances
1051                .lock()
1052                .unwrap()
1053                .insert((balance.owner_id, balance.building_id), balance.clone());
1054            Ok(balance.clone())
1055        }
1056
1057        async fn delete(&self, owner_id: Uuid, building_id: Uuid) -> Result<bool, String> {
1058            Ok(self
1059                .balances
1060                .lock()
1061                .unwrap()
1062                .remove(&(owner_id, building_id))
1063                .is_some())
1064        }
1065
1066        async fn get_leaderboard(
1067            &self,
1068            building_id: Uuid,
1069            limit: i32,
1070        ) -> Result<Vec<OwnerCreditBalance>, String> {
1071            let mut balances: Vec<_> = self
1072                .balances
1073                .lock()
1074                .unwrap()
1075                .values()
1076                .filter(|b| b.building_id == building_id)
1077                .cloned()
1078                .collect();
1079            balances.sort_by_key(|a| std::cmp::Reverse(a.balance));
1080            balances.truncate(limit as usize);
1081            Ok(balances)
1082        }
1083
1084        async fn count_active_participants(&self, building_id: Uuid) -> Result<i64, String> {
1085            Ok(self
1086                .balances
1087                .lock()
1088                .unwrap()
1089                .values()
1090                .filter(|b| b.building_id == building_id && b.total_exchanges > 0)
1091                .count() as i64)
1092        }
1093
1094        async fn get_total_credits_in_circulation(&self, building_id: Uuid) -> Result<i32, String> {
1095            Ok(self
1096                .balances
1097                .lock()
1098                .unwrap()
1099                .values()
1100                .filter(|b| b.building_id == building_id)
1101                .map(|b| b.credits_earned)
1102                .sum())
1103        }
1104    }
1105
1106    // ── Test Helpers ────────────────────────────────────────────────────
1107
1108    fn make_owner(user_id: Uuid) -> Owner {
1109        let now = Utc::now();
1110        Owner {
1111            id: Uuid::new_v4(),
1112            organization_id: Uuid::new_v4(),
1113            user_id: Some(user_id),
1114            first_name: "Jean".to_string(),
1115            last_name: "Dupont".to_string(),
1116            email: "jean@example.com".to_string(),
1117            phone: None,
1118            address: "1 rue de la Loi".to_string(),
1119            city: "Bruxelles".to_string(),
1120            postal_code: "1000".to_string(),
1121            country: "BE".to_string(),
1122            created_at: now,
1123            updated_at: now,
1124        }
1125    }
1126
1127    fn make_owner_with_name(user_id: Uuid, first: &str, last: &str) -> Owner {
1128        let now = Utc::now();
1129        Owner {
1130            id: Uuid::new_v4(),
1131            organization_id: Uuid::new_v4(),
1132            user_id: Some(user_id),
1133            first_name: first.to_string(),
1134            last_name: last.to_string(),
1135            email: format!("{}@example.com", first.to_lowercase()),
1136            phone: None,
1137            address: "1 rue de la Loi".to_string(),
1138            city: "Bruxelles".to_string(),
1139            postal_code: "1000".to_string(),
1140            country: "BE".to_string(),
1141            created_at: now,
1142            updated_at: now,
1143        }
1144    }
1145
1146    fn setup_use_cases(
1147        owner_repo: Arc<MockOwnerRepository>,
1148        exchange_repo: Arc<MockLocalExchangeRepository>,
1149        balance_repo: Arc<MockOwnerCreditBalanceRepository>,
1150    ) -> LocalExchangeUseCases {
1151        LocalExchangeUseCases::new(exchange_repo, balance_repo, owner_repo)
1152    }
1153
1154    // ── Tests ───────────────────────────────────────────────────────────
1155
1156    #[tokio::test]
1157    async fn test_create_exchange_success() {
1158        let owner_repo = Arc::new(MockOwnerRepository::new());
1159        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1160        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1161
1162        let user_id = Uuid::new_v4();
1163        let provider = make_owner_with_name(user_id, "Alice", "Martin");
1164        owner_repo.insert(provider.clone());
1165
1166        let building_id = Uuid::new_v4();
1167        let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1168
1169        let dto = CreateLocalExchangeDto {
1170            building_id,
1171            exchange_type: ExchangeType::Service,
1172            title: "Gardening help".to_string(),
1173            description: "I can help with your garden".to_string(),
1174            credits: 3,
1175        };
1176
1177        let result = uc.create_exchange(user_id, dto).await;
1178        assert!(result.is_ok(), "create_exchange failed: {:?}", result.err());
1179
1180        let resp = result.unwrap();
1181        assert_eq!(resp.building_id, building_id);
1182        assert_eq!(resp.provider_id, provider.id);
1183        assert_eq!(resp.provider_name, "Alice Martin");
1184        assert_eq!(resp.status, ExchangeStatus::Offered);
1185        assert_eq!(resp.credits, 3);
1186        assert_eq!(resp.title, "Gardening help");
1187        assert!(resp.requester_id.is_none());
1188    }
1189
1190    #[tokio::test]
1191    async fn test_request_exchange_success() {
1192        let owner_repo = Arc::new(MockOwnerRepository::new());
1193        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1194        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1195
1196        let provider_user_id = Uuid::new_v4();
1197        let requester_user_id = Uuid::new_v4();
1198        let building_id = Uuid::new_v4();
1199
1200        let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1201        let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1202        owner_repo.insert(provider.clone());
1203        owner_repo.insert(requester.clone());
1204
1205        let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1206
1207        // Create an exchange first
1208        let dto = CreateLocalExchangeDto {
1209            building_id,
1210            exchange_type: ExchangeType::ObjectLoan,
1211            title: "Drill to lend".to_string(),
1212            description: "Heavy-duty drill available".to_string(),
1213            credits: 1,
1214        };
1215        let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1216
1217        // Now requester requests it
1218        let result = uc
1219            .request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1220            .await;
1221        assert!(
1222            result.is_ok(),
1223            "request_exchange failed: {:?}",
1224            result.err()
1225        );
1226
1227        let resp = result.unwrap();
1228        assert_eq!(resp.status, ExchangeStatus::Requested);
1229        assert_eq!(resp.requester_id, Some(requester.id));
1230        assert_eq!(resp.requester_name, Some("Bob Leroy".to_string()));
1231    }
1232
1233    #[tokio::test]
1234    async fn test_start_exchange_success() {
1235        let owner_repo = Arc::new(MockOwnerRepository::new());
1236        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1237        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1238
1239        let provider_user_id = Uuid::new_v4();
1240        let requester_user_id = Uuid::new_v4();
1241        let building_id = Uuid::new_v4();
1242
1243        let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1244        let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1245        owner_repo.insert(provider.clone());
1246        owner_repo.insert(requester.clone());
1247
1248        let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo);
1249
1250        // Create + request
1251        let dto = CreateLocalExchangeDto {
1252            building_id,
1253            exchange_type: ExchangeType::Service,
1254            title: "Plumbing fix".to_string(),
1255            description: "Fix leaking pipe".to_string(),
1256            credits: 2,
1257        };
1258        let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1259        uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1260            .await
1261            .unwrap();
1262
1263        // Provider starts the exchange
1264        let result = uc.start_exchange(created.id, provider_user_id).await;
1265        assert!(result.is_ok(), "start_exchange failed: {:?}", result.err());
1266
1267        let resp = result.unwrap();
1268        assert_eq!(resp.status, ExchangeStatus::InProgress);
1269        assert!(resp.started_at.is_some());
1270    }
1271
1272    #[tokio::test]
1273    async fn test_complete_exchange_updates_credit_balances() {
1274        let owner_repo = Arc::new(MockOwnerRepository::new());
1275        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1276        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1277
1278        let provider_user_id = Uuid::new_v4();
1279        let requester_user_id = Uuid::new_v4();
1280        let building_id = Uuid::new_v4();
1281
1282        let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1283        let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1284        owner_repo.insert(provider.clone());
1285        owner_repo.insert(requester.clone());
1286
1287        let uc = setup_use_cases(
1288            owner_repo.clone(),
1289            exchange_repo.clone(),
1290            balance_repo.clone(),
1291        );
1292
1293        // Create + request + start
1294        let credits = 5;
1295        let dto = CreateLocalExchangeDto {
1296            building_id,
1297            exchange_type: ExchangeType::Service,
1298            title: "IT Help".to_string(),
1299            description: "Install software".to_string(),
1300            credits,
1301        };
1302        let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1303        uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1304            .await
1305            .unwrap();
1306        uc.start_exchange(created.id, provider_user_id)
1307            .await
1308            .unwrap();
1309
1310        // Provider completes the exchange
1311        let result = uc
1312            .complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1313            .await;
1314        assert!(
1315            result.is_ok(),
1316            "complete_exchange failed: {:?}",
1317            result.err()
1318        );
1319
1320        let resp = result.unwrap();
1321        assert_eq!(resp.status, ExchangeStatus::Completed);
1322        assert!(resp.completed_at.is_some());
1323
1324        // Verify credit balances were updated
1325        let provider_balance = uc
1326            .get_credit_balance(provider.id, building_id)
1327            .await
1328            .unwrap();
1329        assert_eq!(provider_balance.credits_earned, credits);
1330        assert_eq!(provider_balance.balance, credits);
1331        assert_eq!(provider_balance.total_exchanges, 1);
1332
1333        let requester_balance = uc
1334            .get_credit_balance(requester.id, building_id)
1335            .await
1336            .unwrap();
1337        assert_eq!(requester_balance.credits_spent, credits);
1338        assert_eq!(requester_balance.balance, -credits);
1339        assert_eq!(requester_balance.total_exchanges, 1);
1340    }
1341
1342    #[tokio::test]
1343    async fn test_cancel_exchange_success() {
1344        let owner_repo = Arc::new(MockOwnerRepository::new());
1345        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1346        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1347
1348        let provider_user_id = Uuid::new_v4();
1349        let requester_user_id = Uuid::new_v4();
1350        let building_id = Uuid::new_v4();
1351
1352        let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1353        let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1354        owner_repo.insert(provider.clone());
1355        owner_repo.insert(requester.clone());
1356
1357        let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1358
1359        // Create + request
1360        let dto = CreateLocalExchangeDto {
1361            building_id,
1362            exchange_type: ExchangeType::SharedPurchase,
1363            title: "Bulk order".to_string(),
1364            description: "Shared cleaning supplies".to_string(),
1365            credits: 2,
1366        };
1367        let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1368        uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1369            .await
1370            .unwrap();
1371
1372        // Requester cancels with reason
1373        let cancel_dto = CancelExchangeDto {
1374            reason: Some("Changed my mind".to_string()),
1375        };
1376        let result = uc
1377            .cancel_exchange(created.id, requester_user_id, cancel_dto)
1378            .await;
1379        assert!(result.is_ok(), "cancel_exchange failed: {:?}", result.err());
1380
1381        let resp = result.unwrap();
1382        assert_eq!(resp.status, ExchangeStatus::Cancelled);
1383        assert!(resp.cancelled_at.is_some());
1384        assert_eq!(
1385            resp.cancellation_reason,
1386            Some("Changed my mind".to_string())
1387        );
1388    }
1389
1390    #[tokio::test]
1391    async fn test_rate_provider_success() {
1392        let owner_repo = Arc::new(MockOwnerRepository::new());
1393        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1394        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1395
1396        let provider_user_id = Uuid::new_v4();
1397        let requester_user_id = Uuid::new_v4();
1398        let building_id = Uuid::new_v4();
1399
1400        let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1401        let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1402        owner_repo.insert(provider.clone());
1403        owner_repo.insert(requester.clone());
1404
1405        let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1406
1407        // Full workflow: create + request + start + complete
1408        let dto = CreateLocalExchangeDto {
1409            building_id,
1410            exchange_type: ExchangeType::Service,
1411            title: "Painting".to_string(),
1412            description: "Paint the hallway".to_string(),
1413            credits: 4,
1414        };
1415        let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1416        uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1417            .await
1418            .unwrap();
1419        uc.start_exchange(created.id, provider_user_id)
1420            .await
1421            .unwrap();
1422        uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1423            .await
1424            .unwrap();
1425
1426        // Requester rates provider
1427        let rate_dto = RateExchangeDto { rating: 5 };
1428        let result = uc
1429            .rate_provider(created.id, requester_user_id, rate_dto)
1430            .await;
1431        assert!(result.is_ok(), "rate_provider failed: {:?}", result.err());
1432
1433        let resp = result.unwrap();
1434        assert_eq!(resp.provider_rating, Some(5));
1435
1436        // Verify provider's average rating was updated in balance
1437        let provider_balance = balance_repo
1438            .find_by_owner_and_building(provider.id, building_id)
1439            .await
1440            .unwrap()
1441            .unwrap();
1442        assert_eq!(provider_balance.average_rating, Some(5.0));
1443    }
1444
1445    #[tokio::test]
1446    async fn test_rate_requester_success() {
1447        let owner_repo = Arc::new(MockOwnerRepository::new());
1448        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1449        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1450
1451        let provider_user_id = Uuid::new_v4();
1452        let requester_user_id = Uuid::new_v4();
1453        let building_id = Uuid::new_v4();
1454
1455        let provider = make_owner_with_name(provider_user_id, "Alice", "Martin");
1456        let requester = make_owner_with_name(requester_user_id, "Bob", "Leroy");
1457        owner_repo.insert(provider.clone());
1458        owner_repo.insert(requester.clone());
1459
1460        let uc = setup_use_cases(owner_repo, exchange_repo.clone(), balance_repo.clone());
1461
1462        // Full workflow: create + request + start + complete
1463        let dto = CreateLocalExchangeDto {
1464            building_id,
1465            exchange_type: ExchangeType::Service,
1466            title: "Babysitting".to_string(),
1467            description: "Watch kids for 3 hours".to_string(),
1468            credits: 3,
1469        };
1470        let created = uc.create_exchange(provider_user_id, dto).await.unwrap();
1471        uc.request_exchange(created.id, requester_user_id, RequestExchangeDto {})
1472            .await
1473            .unwrap();
1474        uc.start_exchange(created.id, provider_user_id)
1475            .await
1476            .unwrap();
1477        uc.complete_exchange(created.id, provider_user_id, CompleteExchangeDto {})
1478            .await
1479            .unwrap();
1480
1481        // Provider rates requester
1482        let rate_dto = RateExchangeDto { rating: 4 };
1483        let result = uc
1484            .rate_requester(created.id, provider_user_id, rate_dto)
1485            .await;
1486        assert!(result.is_ok(), "rate_requester failed: {:?}", result.err());
1487
1488        let resp = result.unwrap();
1489        assert_eq!(resp.requester_rating, Some(4));
1490
1491        // Verify requester's average rating was updated
1492        let requester_balance = balance_repo
1493            .find_by_owner_and_building(requester.id, building_id)
1494            .await
1495            .unwrap()
1496            .unwrap();
1497        assert_eq!(requester_balance.average_rating, Some(4.0));
1498    }
1499
1500    #[tokio::test]
1501    async fn test_get_credit_balance_creates_if_missing() {
1502        let owner_repo = Arc::new(MockOwnerRepository::new());
1503        let exchange_repo = Arc::new(MockLocalExchangeRepository::new());
1504        let balance_repo = Arc::new(MockOwnerCreditBalanceRepository::new());
1505
1506        let user_id = Uuid::new_v4();
1507        let owner = make_owner(user_id);
1508        let owner_id = owner.id;
1509        let building_id = Uuid::new_v4();
1510        owner_repo.insert(owner);
1511
1512        let uc = setup_use_cases(owner_repo, exchange_repo, balance_repo);
1513
1514        // No balance exists yet; get_credit_balance should auto-create via get_or_create
1515        let result = uc.get_credit_balance(owner_id, building_id).await;
1516        assert!(
1517            result.is_ok(),
1518            "get_credit_balance failed: {:?}",
1519            result.err()
1520        );
1521
1522        let balance = result.unwrap();
1523        assert_eq!(balance.owner_id, owner_id);
1524        assert_eq!(balance.building_id, building_id);
1525        assert_eq!(balance.credits_earned, 0);
1526        assert_eq!(balance.credits_spent, 0);
1527        assert_eq!(balance.balance, 0);
1528        assert_eq!(balance.total_exchanges, 0);
1529    }
1530}