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