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    /// Create a new exchange offer
34    pub async fn create_exchange(
35        &self,
36        provider_id: Uuid, // From auth
37        dto: CreateLocalExchangeDto,
38    ) -> Result<LocalExchangeResponseDto, String> {
39        // Verify owner exists
40        let provider = self
41            .owner_repo
42            .find_by_id(provider_id)
43            .await?
44            .ok_or("Provider not found".to_string())?;
45
46        // Verify building exists (via owner)
47        let _ = self
48            .owner_repo
49            .find_by_id(dto.building_id) // Check building ownership
50            .await?;
51
52        // Create domain entity
53        let exchange = LocalExchange::new(
54            dto.building_id,
55            provider_id,
56            dto.exchange_type,
57            dto.title,
58            dto.description,
59            dto.credits,
60        )?;
61
62        // Persist
63        let created = self.exchange_repo.create(&exchange).await?;
64
65        // Return DTO with provider name
66        Ok(LocalExchangeResponseDto::from_entity(
67            created,
68            format!("{} {}", provider.first_name, provider.last_name),
69            None,
70        ))
71    }
72
73    /// Get exchange by ID
74    pub async fn get_exchange(&self, id: Uuid) -> Result<LocalExchangeResponseDto, String> {
75        let exchange = self
76            .exchange_repo
77            .find_by_id(id)
78            .await?
79            .ok_or("Exchange not found".to_string())?;
80
81        // Get provider name
82        let provider = self
83            .owner_repo
84            .find_by_id(exchange.provider_id)
85            .await?
86            .ok_or("Provider not found".to_string())?;
87        let provider_name = format!("{} {}", provider.first_name, provider.last_name);
88
89        // Get requester name if exists
90        let requester_name = if let Some(requester_id) = exchange.requester_id {
91            let requester = self.owner_repo.find_by_id(requester_id).await?;
92            requester.map(|r| format!("{} {}", r.first_name, r.last_name))
93        } else {
94            None
95        };
96
97        Ok(LocalExchangeResponseDto::from_entity(
98            exchange,
99            provider_name,
100            requester_name,
101        ))
102    }
103
104    /// List all exchanges for a building
105    pub async fn list_building_exchanges(
106        &self,
107        building_id: Uuid,
108    ) -> Result<Vec<LocalExchangeResponseDto>, String> {
109        let exchanges = self.exchange_repo.find_by_building(building_id).await?;
110
111        self.enrich_exchanges_with_names(exchanges).await
112    }
113
114    /// List available exchanges (status = Offered)
115    pub async fn list_available_exchanges(
116        &self,
117        building_id: Uuid,
118    ) -> Result<Vec<LocalExchangeResponseDto>, String> {
119        let exchanges = self
120            .exchange_repo
121            .find_available_by_building(building_id)
122            .await?;
123
124        self.enrich_exchanges_with_names(exchanges).await
125    }
126
127    /// List exchanges by owner (as provider OR requester)
128    pub async fn list_owner_exchanges(
129        &self,
130        owner_id: Uuid,
131    ) -> Result<Vec<LocalExchangeResponseDto>, String> {
132        let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
133
134        self.enrich_exchanges_with_names(exchanges).await
135    }
136
137    /// List exchanges by type
138    pub async fn list_exchanges_by_type(
139        &self,
140        building_id: Uuid,
141        exchange_type: ExchangeType,
142    ) -> Result<Vec<LocalExchangeResponseDto>, String> {
143        let exchanges = self
144            .exchange_repo
145            .find_by_type(building_id, exchange_type.to_sql())
146            .await?;
147
148        self.enrich_exchanges_with_names(exchanges).await
149    }
150
151    /// Request an exchange (transition: Offered → Requested)
152    pub async fn request_exchange(
153        &self,
154        exchange_id: Uuid,
155        requester_id: Uuid, // From auth
156        _dto: RequestExchangeDto,
157    ) -> Result<LocalExchangeResponseDto, String> {
158        // Load exchange
159        let mut exchange = self
160            .exchange_repo
161            .find_by_id(exchange_id)
162            .await?
163            .ok_or("Exchange not found".to_string())?;
164
165        // Apply business logic
166        exchange.request(requester_id)?;
167
168        // Persist
169        let updated = self.exchange_repo.update(&exchange).await?;
170
171        // Return enriched DTO
172        self.get_exchange(updated.id).await
173    }
174
175    /// Start an exchange (transition: Requested → InProgress)
176    /// Only provider can start
177    pub async fn start_exchange(
178        &self,
179        exchange_id: Uuid,
180        actor_id: Uuid, // From auth (must be provider)
181    ) -> Result<LocalExchangeResponseDto, String> {
182        let mut exchange = self
183            .exchange_repo
184            .find_by_id(exchange_id)
185            .await?
186            .ok_or("Exchange not found".to_string())?;
187
188        exchange.start(actor_id)?;
189
190        let updated = self.exchange_repo.update(&exchange).await?;
191
192        self.get_exchange(updated.id).await
193    }
194
195    /// Complete an exchange (transition: InProgress → Completed)
196    /// Updates credit balances for both parties
197    pub async fn complete_exchange(
198        &self,
199        exchange_id: Uuid,
200        actor_id: Uuid, // From auth (provider or requester)
201        _dto: CompleteExchangeDto,
202    ) -> Result<LocalExchangeResponseDto, String> {
203        let mut exchange = self
204            .exchange_repo
205            .find_by_id(exchange_id)
206            .await?
207            .ok_or("Exchange not found".to_string())?;
208
209        // Get requester before completing (we'll need it for balance update)
210        let requester_id = exchange
211            .requester_id
212            .ok_or("Exchange has no requester".to_string())?;
213
214        exchange.complete(actor_id)?;
215
216        let updated = self.exchange_repo.update(&exchange).await?;
217
218        // Update credit balances
219        self.update_credit_balances_on_completion(&updated).await?;
220
221        // Increment exchange counters
222        let mut provider_balance = self
223            .balance_repo
224            .get_or_create(updated.provider_id, updated.building_id)
225            .await?;
226        provider_balance.increment_exchanges();
227        self.balance_repo.update(&provider_balance).await?;
228
229        let mut requester_balance = self
230            .balance_repo
231            .get_or_create(requester_id, updated.building_id)
232            .await?;
233        requester_balance.increment_exchanges();
234        self.balance_repo.update(&requester_balance).await?;
235
236        self.get_exchange(updated.id).await
237    }
238
239    /// Cancel an exchange
240    pub async fn cancel_exchange(
241        &self,
242        exchange_id: Uuid,
243        actor_id: Uuid, // From auth (provider or requester)
244        dto: CancelExchangeDto,
245    ) -> Result<LocalExchangeResponseDto, String> {
246        let mut exchange = self
247            .exchange_repo
248            .find_by_id(exchange_id)
249            .await?
250            .ok_or("Exchange not found".to_string())?;
251
252        exchange.cancel(actor_id, dto.reason)?;
253
254        let updated = self.exchange_repo.update(&exchange).await?;
255
256        self.get_exchange(updated.id).await
257    }
258
259    /// Rate the provider (by requester)
260    pub async fn rate_provider(
261        &self,
262        exchange_id: Uuid,
263        requester_id: Uuid, // From auth
264        dto: RateExchangeDto,
265    ) -> Result<LocalExchangeResponseDto, String> {
266        let mut exchange = self
267            .exchange_repo
268            .find_by_id(exchange_id)
269            .await?
270            .ok_or("Exchange not found".to_string())?;
271
272        exchange.rate_provider(requester_id, dto.rating)?;
273
274        let updated = self.exchange_repo.update(&exchange).await?;
275
276        // Update provider's average rating
277        self.update_average_rating(updated.provider_id, updated.building_id)
278            .await?;
279
280        self.get_exchange(updated.id).await
281    }
282
283    /// Rate the requester (by provider)
284    pub async fn rate_requester(
285        &self,
286        exchange_id: Uuid,
287        provider_id: Uuid, // From auth
288        dto: RateExchangeDto,
289    ) -> Result<LocalExchangeResponseDto, String> {
290        let mut exchange = self
291            .exchange_repo
292            .find_by_id(exchange_id)
293            .await?
294            .ok_or("Exchange not found".to_string())?;
295
296        exchange.rate_requester(provider_id, dto.rating)?;
297
298        let updated = self.exchange_repo.update(&exchange).await?;
299
300        // Update requester's average rating
301        if let Some(requester_id) = updated.requester_id {
302            self.update_average_rating(requester_id, updated.building_id)
303                .await?;
304        }
305
306        self.get_exchange(updated.id).await
307    }
308
309    /// Delete an exchange (only if not completed)
310    pub async fn delete_exchange(&self, exchange_id: Uuid, actor_id: Uuid) -> Result<(), String> {
311        let exchange = self
312            .exchange_repo
313            .find_by_id(exchange_id)
314            .await?
315            .ok_or("Exchange not found".to_string())?;
316
317        // Only provider can delete
318        if exchange.provider_id != actor_id {
319            return Err("Only the provider can delete the exchange".to_string());
320        }
321
322        // Cannot delete completed exchanges
323        if exchange.status == ExchangeStatus::Completed {
324            return Err("Cannot delete a completed exchange".to_string());
325        }
326
327        self.exchange_repo.delete(exchange_id).await?;
328
329        Ok(())
330    }
331
332    /// Get credit balance for an owner in a building
333    pub async fn get_credit_balance(
334        &self,
335        owner_id: Uuid,
336        building_id: Uuid,
337    ) -> Result<OwnerCreditBalanceDto, String> {
338        let balance = self
339            .balance_repo
340            .get_or_create(owner_id, building_id)
341            .await?;
342
343        let owner = self
344            .owner_repo
345            .find_by_id(owner_id)
346            .await?
347            .ok_or("Owner not found".to_string())?;
348        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
349
350        Ok(OwnerCreditBalanceDto::from_entity(balance, owner_name))
351    }
352
353    /// Get leaderboard (top contributors)
354    pub async fn get_leaderboard(
355        &self,
356        building_id: Uuid,
357        limit: i32,
358    ) -> Result<Vec<OwnerCreditBalanceDto>, String> {
359        let balances = self
360            .balance_repo
361            .get_leaderboard(building_id, limit)
362            .await?;
363
364        let mut dtos = Vec::new();
365        for balance in balances {
366            if let Some(owner) = self.owner_repo.find_by_id(balance.owner_id).await? {
367                let owner_name = format!("{} {}", owner.first_name, owner.last_name);
368                dtos.push(OwnerCreditBalanceDto::from_entity(balance, owner_name));
369            }
370        }
371
372        Ok(dtos)
373    }
374
375    /// Get SEL statistics for a building
376    pub async fn get_statistics(&self, building_id: Uuid) -> Result<SelStatisticsDto, String> {
377        let total_exchanges = self.exchange_repo.count_by_building(building_id).await? as i32;
378
379        let active_exchanges = self
380            .exchange_repo
381            .count_by_building_and_status(building_id, ExchangeStatus::Offered.to_sql())
382            .await? as i32
383            + self
384                .exchange_repo
385                .count_by_building_and_status(building_id, ExchangeStatus::Requested.to_sql())
386                .await? as i32
387            + self
388                .exchange_repo
389                .count_by_building_and_status(building_id, ExchangeStatus::InProgress.to_sql())
390                .await? as i32;
391
392        let completed_exchanges = self
393            .exchange_repo
394            .count_by_building_and_status(building_id, ExchangeStatus::Completed.to_sql())
395            .await? as i32;
396
397        let total_credits_exchanged = self
398            .exchange_repo
399            .get_total_credits_exchanged(building_id)
400            .await?;
401
402        let active_participants = self
403            .balance_repo
404            .count_active_participants(building_id)
405            .await? as i32;
406
407        let average_exchange_rating = self
408            .exchange_repo
409            .get_average_exchange_rating(building_id)
410            .await?;
411
412        // Determine most popular exchange type
413        let service_count = self
414            .exchange_repo
415            .count_by_building_and_status(building_id, ExchangeType::Service.to_sql())
416            .await?;
417        let object_loan_count = self
418            .exchange_repo
419            .count_by_building_and_status(building_id, ExchangeType::ObjectLoan.to_sql())
420            .await?;
421        let shared_purchase_count = self
422            .exchange_repo
423            .count_by_building_and_status(building_id, ExchangeType::SharedPurchase.to_sql())
424            .await?;
425
426        let most_popular_exchange_type =
427            if service_count >= object_loan_count && service_count >= shared_purchase_count {
428                Some(ExchangeType::Service)
429            } else if object_loan_count >= shared_purchase_count {
430                Some(ExchangeType::ObjectLoan)
431            } else {
432                Some(ExchangeType::SharedPurchase)
433            };
434
435        Ok(SelStatisticsDto {
436            building_id,
437            total_exchanges,
438            active_exchanges,
439            completed_exchanges,
440            total_credits_exchanged,
441            active_participants,
442            average_exchange_rating,
443            most_popular_exchange_type,
444        })
445    }
446
447    /// Get owner exchange summary
448    pub async fn get_owner_summary(
449        &self,
450        owner_id: Uuid,
451    ) -> Result<OwnerExchangeSummaryDto, String> {
452        let owner = self
453            .owner_repo
454            .find_by_id(owner_id)
455            .await?
456            .ok_or("Owner not found".to_string())?;
457        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
458
459        let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
460
461        let as_provider = exchanges
462            .iter()
463            .filter(|e| e.provider_id == owner_id)
464            .count() as i32;
465
466        let as_requester = exchanges
467            .iter()
468            .filter(|e| e.requester_id == Some(owner_id))
469            .count() as i32;
470
471        // Calculate average rating (average of all ratings received)
472        let mut total_ratings = 0;
473        let mut rating_count = 0;
474
475        for exchange in &exchanges {
476            if exchange.provider_id == owner_id {
477                if let Some(rating) = exchange.provider_rating {
478                    total_ratings += rating;
479                    rating_count += 1;
480                }
481            }
482            if exchange.requester_id == Some(owner_id) {
483                if let Some(rating) = exchange.requester_rating {
484                    total_ratings += rating;
485                    rating_count += 1;
486                }
487            }
488        }
489
490        let average_rating = if rating_count > 0 {
491            Some(total_ratings as f32 / rating_count as f32)
492        } else {
493            None
494        };
495
496        // Get recent 5 exchanges
497        let recent: Vec<_> = exchanges.into_iter().take(5).collect();
498        let recent_dtos = self.enrich_exchanges_with_names(recent).await?;
499
500        Ok(OwnerExchangeSummaryDto {
501            owner_id,
502            owner_name,
503            as_provider,
504            as_requester,
505            total_exchanges: as_provider + as_requester,
506            average_rating,
507            recent_exchanges: recent_dtos,
508        })
509    }
510
511    // Private helper methods
512
513    /// Update credit balances when exchange is completed
514    async fn update_credit_balances_on_completion(
515        &self,
516        exchange: &LocalExchange,
517    ) -> Result<(), String> {
518        let requester_id = exchange
519            .requester_id
520            .ok_or("Exchange has no requester".to_string())?;
521
522        // Provider earns credits
523        let mut provider_balance = self
524            .balance_repo
525            .get_or_create(exchange.provider_id, exchange.building_id)
526            .await?;
527        provider_balance.earn_credits(exchange.credits)?;
528        self.balance_repo.update(&provider_balance).await?;
529
530        // Requester spends credits
531        let mut requester_balance = self
532            .balance_repo
533            .get_or_create(requester_id, exchange.building_id)
534            .await?;
535        requester_balance.spend_credits(exchange.credits)?;
536        self.balance_repo.update(&requester_balance).await?;
537
538        Ok(())
539    }
540
541    /// Update average rating for an owner
542    async fn update_average_rating(&self, owner_id: Uuid, building_id: Uuid) -> Result<(), String> {
543        let exchanges = self.exchange_repo.find_by_owner(owner_id).await?;
544
545        let mut total_ratings = 0;
546        let mut rating_count = 0;
547
548        for exchange in exchanges {
549            if exchange.provider_id == owner_id {
550                if let Some(rating) = exchange.provider_rating {
551                    total_ratings += rating;
552                    rating_count += 1;
553                }
554            }
555            if exchange.requester_id == Some(owner_id) {
556                if let Some(rating) = exchange.requester_rating {
557                    total_ratings += rating;
558                    rating_count += 1;
559                }
560            }
561        }
562
563        if rating_count > 0 {
564            let average = total_ratings as f32 / rating_count as f32;
565
566            let mut balance = self
567                .balance_repo
568                .get_or_create(owner_id, building_id)
569                .await?;
570            balance.update_rating(average)?;
571            self.balance_repo.update(&balance).await?;
572        }
573
574        Ok(())
575    }
576
577    /// Enrich exchanges with owner names (provider + requester)
578    async fn enrich_exchanges_with_names(
579        &self,
580        exchanges: Vec<LocalExchange>,
581    ) -> Result<Vec<LocalExchangeResponseDto>, String> {
582        let mut dtos = Vec::new();
583
584        for exchange in exchanges {
585            // Get provider name
586            let provider = self.owner_repo.find_by_id(exchange.provider_id).await?;
587            let provider_name = if let Some(p) = provider {
588                format!("{} {}", p.first_name, p.last_name)
589            } else {
590                "Unknown".to_string()
591            };
592
593            // Get requester name if exists
594            let requester_name = if let Some(requester_id) = exchange.requester_id {
595                let requester = self.owner_repo.find_by_id(requester_id).await?;
596                requester.map(|r| format!("{} {}", r.first_name, r.last_name))
597            } else {
598                None
599            };
600
601            dtos.push(LocalExchangeResponseDto::from_entity(
602                exchange,
603                provider_name,
604                requester_name,
605            ));
606        }
607
608        Ok(dtos)
609    }
610}