mudra_cli/
conversion.rs

1//! Core conversion logic and types
2
3use crate::{CurrencyError, Result};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// Represents a currency with validation
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct Currency {
10    code: String,
11}
12
13impl Currency {
14    /// Create a new currency with validation
15    pub fn new<S: Into<String>>(code: S) -> Result<Self> {
16        let code = code.into().to_uppercase();
17
18        if !crate::api::is_valid_currency_code(&code) {
19            return Err(CurrencyError::invalid_currency(&code));
20        }
21
22        Ok(Currency { code })
23    }
24
25    /// Get the currency code
26    pub fn code(&self) -> &str {
27        &self.code
28    }
29
30    /// Check if this is the same currency as another
31    pub fn is_same_as(&self, other: &Currency) -> bool {
32        self.code == other.code
33    }
34}
35
36impl fmt::Display for Currency {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}", self.code)
39    }
40}
41
42impl TryFrom<&str> for Currency {
43    type Error = CurrencyError;
44
45    fn try_from(code: &str) -> Result<Self> {
46        Currency::new(code)
47    }
48}
49
50impl TryFrom<String> for Currency {
51    type Error = CurrencyError;
52
53    fn try_from(code: String) -> Result<Self> {
54        Currency::new(code)
55    }
56}
57
58/// Represents a monetary amount with a specific currency
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60pub struct Money {
61    amount: f64,
62    currency: Currency,
63}
64
65impl Money {
66    /// Create a new Money instance
67    pub fn new(amount: f64, currency: Currency) -> Result<Self> {
68        Self::validate_amount(amount)?;
69        Ok(Money { amount, currency })
70    }
71
72    /// Create Money from amount and currency code
73    pub fn from_code<S: Into<String>>(amount: f64, currency_code: S) -> Result<Self> {
74        let currency = Currency::new(currency_code)?;
75        Self::new(amount, currency)
76    }
77
78    /// Get the amount
79    pub fn amount(&self) -> f64 {
80        self.amount
81    }
82
83    /// Get the currency
84    pub fn currency(&self) -> &Currency {
85        &self.currency
86    }
87
88    /// Round to specified decimal places
89    pub fn round(&self, decimal_places: u32) -> Self {
90        let multiplier = 10_f64.powi(decimal_places as i32);
91        let rounded_amount = (self.amount * multiplier).round() / multiplier;
92
93        Money {
94            amount: rounded_amount,
95            currency: self.currency.clone(),
96        }
97    }
98
99    /// Validate amount for financial calculations
100    fn validate_amount(amount: f64) -> Result<()> {
101        if amount < 0.0 {
102            return Err(CurrencyError::invalid_amount(amount));
103        }
104        if amount.is_nan() || amount.is_infinite() {
105            return Err(CurrencyError::invalid_amount(amount));
106        }
107        // Reasonable upper limit for financial calculations
108        if amount > 1_000_000_000_000.0 {
109            return Err(CurrencyError::invalid_amount(amount));
110        }
111        Ok(())
112    }
113}
114
115impl fmt::Display for Money {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "{:.2} {}", self.amount, self.currency)
118    }
119}
120
121/// Request for currency conversion
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ConversionRequest {
124    /// Source money (amount + currency)
125    pub from: Money,
126    /// Target currency
127    pub to: Currency,
128}
129
130impl ConversionRequest {
131    /// Create a new conversion request
132    pub fn new(from: Money, to: Currency) -> Self {
133        ConversionRequest { from, to }
134    }
135
136    /// Create from individual components
137    pub fn from_components(amount: f64, from_currency: &str, to_currency: &str) -> Result<Self> {
138        let from = Money::from_code(amount, from_currency)?;
139        let to = Currency::new(to_currency)?;
140        Ok(ConversionRequest::new(from, to))
141    }
142
143    /// Check if this is a same-currency conversion
144    pub fn is_same_currency(&self) -> bool {
145        self.from.currency.is_same_as(&self.to)
146    }
147}
148
149/// Result of a currency conversion
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ConversionResult {
152    /// Original request
153    pub request: ConversionRequest,
154    /// Converted amount
155    pub result: Money,
156    /// Exchange rate used (from -> to)
157    pub exchange_rate: f64,
158    /// Timestamp of the conversion
159    pub timestamp: i64,
160    /// Whether this was a direct or cross-conversion
161    pub conversion_type: ConversionType,
162}
163
164/// Type of conversion performed
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub enum ConversionType {
167    /// Same currency (no conversion needed)
168    SameCurrency,
169    /// Direct conversion (both currencies available in rates)
170    Direct,
171    /// Cross conversion via base currency
172    Cross { via_currency: String },
173}
174
175impl ConversionResult {
176    /// Create a new conversion result
177    pub fn new(
178        request: ConversionRequest,
179        result: Money,
180        exchange_rate: f64,
181        conversion_type: ConversionType,
182    ) -> Self {
183        ConversionResult {
184            request,
185            result,
186            exchange_rate,
187            timestamp: std::time::SystemTime::now()
188                .duration_since(std::time::UNIX_EPOCH)
189                .unwrap_or_default()
190                .as_secs() as i64,
191            conversion_type,
192        }
193    }
194
195    /// Get a formatted summary of the conversion
196    pub fn summary(&self) -> String {
197        match self.conversion_type {
198            ConversionType::SameCurrency => {
199                format!("{} (same currency)", self.request.from)
200            }
201            ConversionType::Direct => {
202                format!(
203                    "{} → {} (rate: {:.6})",
204                    self.request.from, self.result, self.exchange_rate
205                )
206            }
207            ConversionType::Cross { ref via_currency } => {
208                format!(
209                    "{} → {} via {} (rate: {:.6})",
210                    self.request.from, self.result, via_currency, self.exchange_rate
211                )
212            }
213        }
214    }
215}
216
217impl fmt::Display for ConversionResult {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        write!(f, "{}", self.summary())
220    }
221}
222
223#[cfg(test)]
224mod tests;