1use crate::{CurrencyError, Result};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct Currency {
10 code: String,
11}
12
13impl Currency {
14 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 pub fn code(&self) -> &str {
27 &self.code
28 }
29
30 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60pub struct Money {
61 amount: f64,
62 currency: Currency,
63}
64
65impl Money {
66 pub fn new(amount: f64, currency: Currency) -> Result<Self> {
68 Self::validate_amount(amount)?;
69 Ok(Money { amount, currency })
70 }
71
72 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 pub fn amount(&self) -> f64 {
80 self.amount
81 }
82
83 pub fn currency(&self) -> &Currency {
85 &self.currency
86 }
87
88 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ConversionRequest {
124 pub from: Money,
126 pub to: Currency,
128}
129
130impl ConversionRequest {
131 pub fn new(from: Money, to: Currency) -> Self {
133 ConversionRequest { from, to }
134 }
135
136 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 pub fn is_same_currency(&self) -> bool {
145 self.from.currency.is_same_as(&self.to)
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ConversionResult {
152 pub request: ConversionRequest,
154 pub result: Money,
156 pub exchange_rate: f64,
158 pub timestamp: i64,
160 pub conversion_type: ConversionType,
162}
163
164#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub enum ConversionType {
167 SameCurrency,
169 Direct,
171 Cross { via_currency: String },
173}
174
175impl ConversionResult {
176 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 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;