Skip to main content

mudra_cli/
converter.rs

1//! Currency conversion engine
2
3use crate::{
4    CurrencyError, Result,
5    api::ExchangeRateService,
6    conversion::{ConversionRequest, ConversionResult, ConversionType, Currency, Money},
7};
8
9/// Core currency conversion engine
10#[derive(Debug, Clone)]
11pub struct CurrencyConverter {
12    pub exchange_service: ExchangeRateService,
13}
14
15impl CurrencyConverter {
16    /// Create a new converter with an exchange rate service
17    pub fn new(exchange_service: ExchangeRateService) -> Self {
18        CurrencyConverter { exchange_service }
19    }
20
21    /// Create a converter from environment variables
22    pub fn from_env() -> Result<Self> {
23        let service = ExchangeRateService::from_env()?;
24        Ok(CurrencyConverter::new(service))
25    }
26
27    /// Convert currency using the most efficient method available
28    pub async fn convert(&self, request: ConversionRequest) -> Result<ConversionResult> {
29        // Handle same currency conversion (no API call needed)
30        if request.is_same_currency() {
31            return Ok(self.same_currency_conversion(request));
32        }
33
34        // Try direct conversion first (most efficient)
35        if let Ok(result) = self.try_direct_conversion(&request).await {
36            return Ok(result);
37        }
38
39        // Fall back to cross conversion via USD
40        self.cross_conversion(&request, "USD").await
41    }
42
43    /// Convert multiple currencies in parallel
44    pub async fn convert_batch(
45        &self,
46        requests: Vec<ConversionRequest>,
47    ) -> Vec<Result<ConversionResult>> {
48        use futures::future::join_all;
49
50        let futures = requests.into_iter().map(|req| self.convert(req));
51        join_all(futures).await
52    }
53
54    /// Handle same currency conversion (amount stays the same)
55    fn same_currency_conversion(&self, request: ConversionRequest) -> ConversionResult {
56        let result_money = Money::new(request.from.amount(), request.to.clone()).unwrap(); // Safe because we know the currency is valid
57
58        ConversionResult::new(
59            request,
60            result_money,
61            1.0, // Exchange rate is 1.0 for same currency
62            ConversionType::SameCurrency,
63        )
64    }
65
66    /// Try direct conversion (both currencies available in same rate response)
67    async fn try_direct_conversion(&self, request: &ConversionRequest) -> Result<ConversionResult> {
68        // Try with 'from' currency as base
69        if let Ok(result) = self
70            .direct_conversion_with_base(request, request.from.currency().code())
71            .await
72        {
73            return Ok(result);
74        }
75
76        // Try with 'to' currency as base
77        self.direct_conversion_with_base(request, request.to.code())
78            .await
79    }
80
81    /// Perform direct conversion using specified base currency
82    async fn direct_conversion_with_base(
83        &self,
84        request: &ConversionRequest,
85        base_currency: &str,
86    ) -> Result<ConversionResult> {
87        let rates = self
88            .exchange_service
89            .get_latest_rates(base_currency)
90            .await?;
91
92        let exchange_rate = if base_currency == request.from.currency().code() {
93            // Base -> Target conversion
94            rates.get_rate(request.to.code()).ok_or_else(|| {
95                CurrencyError::api(format!(
96                    "Exchange rate not available for {}",
97                    request.to.code()
98                ))
99            })?
100        } else if base_currency == request.to.code() {
101            // Target -> Base conversion (need to invert)
102            let rate_to_base = rates
103                .get_rate(request.from.currency().code())
104                .ok_or_else(|| {
105                    CurrencyError::api(format!(
106                        "Exchange rate not available for {}",
107                        request.from.currency().code()
108                    ))
109                })?;
110
111            if rate_to_base == 0.0 {
112                return Err(CurrencyError::conversion(
113                    "Cannot divide by zero exchange rate",
114                ));
115            }
116
117            1.0 / rate_to_base
118        } else {
119            return Err(CurrencyError::conversion("Neither currency matches base"));
120        };
121
122        let converted_amount = request.from.amount() * exchange_rate;
123        let result_money = Money::new(converted_amount, request.to.clone())?;
124
125        Ok(ConversionResult::new(
126            request.clone(),
127            result_money,
128            exchange_rate,
129            ConversionType::Direct,
130        ))
131    }
132
133    /// Perform cross conversion via intermediate currency (usually USD)
134    async fn cross_conversion(
135        &self,
136        request: &ConversionRequest,
137        via_currency: &str,
138    ) -> Result<ConversionResult> {
139        // Get rates with intermediate currency as base
140        let rates = self.exchange_service.get_latest_rates(via_currency).await?;
141
142        // Get rates for both currencies
143        let from_rate = rates
144            .get_rate(request.from.currency().code())
145            .ok_or_else(|| {
146                CurrencyError::api(format!(
147                    "Exchange rate not available for {}",
148                    request.from.currency().code()
149                ))
150            })?;
151
152        let to_rate = rates.get_rate(request.to.code()).ok_or_else(|| {
153            CurrencyError::api(format!(
154                "Exchange rate not available for {}",
155                request.to.code()
156            ))
157        })?;
158
159        if from_rate == 0.0 {
160            return Err(CurrencyError::conversion(
161                "Cannot divide by zero exchange rate",
162            ));
163        }
164
165        // Calculate cross rate: (from -> via) -> (via -> to)
166        // If via currency = 1, from currency = from_rate, to currency = to_rate
167        // So from -> to rate = to_rate / from_rate
168        let cross_rate = to_rate / from_rate;
169
170        let converted_amount = request.from.amount() * cross_rate;
171        let result_money = Money::new(converted_amount, request.to.clone())?;
172
173        Ok(ConversionResult::new(
174            request.clone(),
175            result_money,
176            cross_rate,
177            ConversionType::Cross {
178                via_currency: via_currency.to_string(),
179            },
180        ))
181    }
182
183    /// Get available currencies from the exchange service
184    pub async fn get_available_currencies(&self) -> Result<Vec<String>> {
185        let rates = self.exchange_service.get_latest_rates("USD").await?;
186        let mut currencies = rates.available_currencies();
187        currencies.push("USD".to_string()); // Base currency isn't included in rates
188        currencies.sort();
189        Ok(currencies)
190    }
191
192    /// Check if a currency is supported
193    pub async fn is_currency_supported(&self, currency: &str) -> Result<bool> {
194        self.exchange_service.is_currency_supported(currency).await
195    }
196
197    /// Get current exchange rate between two currencies
198    pub async fn get_exchange_rate(&self, from: &str, to: &str) -> Result<f64> {
199        let from_currency = Currency::new(from)?;
200        let to_currency = Currency::new(to)?;
201
202        if from_currency.is_same_as(&to_currency) {
203            return Ok(1.0);
204        }
205
206        // Create a minimal conversion request to get the rate
207        let request = ConversionRequest::from_components(1.0, from, to)?;
208        let result = self.convert(request).await?;
209
210        Ok(result.exchange_rate)
211    }
212}