mudra-cli 0.1.0

A robust, high-performance currency converter with caching and CLI interface
Documentation
//! Currency conversion engine

use crate::{
    CurrencyError, Result,
    api::ExchangeRateService,
    conversion::{ConversionRequest, ConversionResult, ConversionType, Currency, Money},
};

/// Core currency conversion engine
#[derive(Debug, Clone)]
pub struct CurrencyConverter {
    pub exchange_service: ExchangeRateService,
}

impl CurrencyConverter {
    /// Create a new converter with an exchange rate service
    pub fn new(exchange_service: ExchangeRateService) -> Self {
        CurrencyConverter { exchange_service }
    }

    /// Create a converter from environment variables
    pub fn from_env() -> Result<Self> {
        let service = ExchangeRateService::from_env()?;
        Ok(CurrencyConverter::new(service))
    }

    /// Convert currency using the most efficient method available
    pub async fn convert(&self, request: ConversionRequest) -> Result<ConversionResult> {
        // Handle same currency conversion (no API call needed)
        if request.is_same_currency() {
            return Ok(self.same_currency_conversion(request));
        }

        // Try direct conversion first (most efficient)
        if let Ok(result) = self.try_direct_conversion(&request).await {
            return Ok(result);
        }

        // Fall back to cross conversion via USD
        self.cross_conversion(&request, "USD").await
    }

    /// Convert multiple currencies in parallel
    pub async fn convert_batch(
        &self,
        requests: Vec<ConversionRequest>,
    ) -> Vec<Result<ConversionResult>> {
        use futures::future::join_all;

        let futures = requests.into_iter().map(|req| self.convert(req));
        join_all(futures).await
    }

    /// Handle same currency conversion (amount stays the same)
    fn same_currency_conversion(&self, request: ConversionRequest) -> ConversionResult {
        let result_money = Money::new(request.from.amount(), request.to.clone()).unwrap(); // Safe because we know the currency is valid

        ConversionResult::new(
            request,
            result_money,
            1.0, // Exchange rate is 1.0 for same currency
            ConversionType::SameCurrency,
        )
    }

    /// Try direct conversion (both currencies available in same rate response)
    async fn try_direct_conversion(&self, request: &ConversionRequest) -> Result<ConversionResult> {
        // Try with 'from' currency as base
        if let Ok(result) = self
            .direct_conversion_with_base(request, request.from.currency().code())
            .await
        {
            return Ok(result);
        }

        // Try with 'to' currency as base
        self.direct_conversion_with_base(request, request.to.code())
            .await
    }

    /// Perform direct conversion using specified base currency
    async fn direct_conversion_with_base(
        &self,
        request: &ConversionRequest,
        base_currency: &str,
    ) -> Result<ConversionResult> {
        let rates = self
            .exchange_service
            .get_latest_rates(base_currency)
            .await?;

        let exchange_rate = if base_currency == request.from.currency().code() {
            // Base -> Target conversion
            rates.get_rate(request.to.code()).ok_or_else(|| {
                CurrencyError::api(format!(
                    "Exchange rate not available for {}",
                    request.to.code()
                ))
            })?
        } else if base_currency == request.to.code() {
            // Target -> Base conversion (need to invert)
            let rate_to_base = rates
                .get_rate(request.from.currency().code())
                .ok_or_else(|| {
                    CurrencyError::api(format!(
                        "Exchange rate not available for {}",
                        request.from.currency().code()
                    ))
                })?;

            if rate_to_base == 0.0 {
                return Err(CurrencyError::conversion(
                    "Cannot divide by zero exchange rate",
                ));
            }

            1.0 / rate_to_base
        } else {
            return Err(CurrencyError::conversion("Neither currency matches base"));
        };

        let converted_amount = request.from.amount() * exchange_rate;
        let result_money = Money::new(converted_amount, request.to.clone())?;

        Ok(ConversionResult::new(
            request.clone(),
            result_money,
            exchange_rate,
            ConversionType::Direct,
        ))
    }

    /// Perform cross conversion via intermediate currency (usually USD)
    async fn cross_conversion(
        &self,
        request: &ConversionRequest,
        via_currency: &str,
    ) -> Result<ConversionResult> {
        // Get rates with intermediate currency as base
        let rates = self.exchange_service.get_latest_rates(via_currency).await?;

        // Get rates for both currencies
        let from_rate = rates
            .get_rate(request.from.currency().code())
            .ok_or_else(|| {
                CurrencyError::api(format!(
                    "Exchange rate not available for {}",
                    request.from.currency().code()
                ))
            })?;

        let to_rate = rates.get_rate(request.to.code()).ok_or_else(|| {
            CurrencyError::api(format!(
                "Exchange rate not available for {}",
                request.to.code()
            ))
        })?;

        if from_rate == 0.0 {
            return Err(CurrencyError::conversion(
                "Cannot divide by zero exchange rate",
            ));
        }

        // Calculate cross rate: (from -> via) -> (via -> to)
        // If via currency = 1, from currency = from_rate, to currency = to_rate
        // So from -> to rate = to_rate / from_rate
        let cross_rate = to_rate / from_rate;

        let converted_amount = request.from.amount() * cross_rate;
        let result_money = Money::new(converted_amount, request.to.clone())?;

        Ok(ConversionResult::new(
            request.clone(),
            result_money,
            cross_rate,
            ConversionType::Cross {
                via_currency: via_currency.to_string(),
            },
        ))
    }

    /// Get available currencies from the exchange service
    pub async fn get_available_currencies(&self) -> Result<Vec<String>> {
        let rates = self.exchange_service.get_latest_rates("USD").await?;
        let mut currencies = rates.available_currencies();
        currencies.push("USD".to_string()); // Base currency isn't included in rates
        currencies.sort();
        Ok(currencies)
    }

    /// Check if a currency is supported
    pub async fn is_currency_supported(&self, currency: &str) -> Result<bool> {
        self.exchange_service.is_currency_supported(currency).await
    }

    /// Get current exchange rate between two currencies
    pub async fn get_exchange_rate(&self, from: &str, to: &str) -> Result<f64> {
        let from_currency = Currency::new(from)?;
        let to_currency = Currency::new(to)?;

        if from_currency.is_same_as(&to_currency) {
            return Ok(1.0);
        }

        // Create a minimal conversion request to get the rate
        let request = ConversionRequest::from_components(1.0, from, to)?;
        let result = self.convert(request).await?;

        Ok(result.exchange_rate)
    }
}