1use crate::{
4 CurrencyError, Result,
5 api::{
6 client::CurrencyClient,
7 types::{
8 ApiErrorResponse, ConversionResponse, ExchangeRateResponse,
9 HistoricalConversionRequest, HistoricalRateResponse, SupportedCurrenciesResponse,
10 is_valid_currency_code, is_valid_date_format,
11 },
12 },
13 cache::{CacheKey, ExchangeRateCache},
14};
15
16#[derive(Debug, Clone)]
18pub struct ExchangeRateService {
19 client: CurrencyClient,
20 cache: ExchangeRateCache,
21}
22
23impl ExchangeRateService {
24 pub fn new(client: CurrencyClient) -> Self {
26 Self {
27 client,
28 cache: ExchangeRateCache::new(),
29 }
30 }
31
32 pub fn from_env() -> Result<Self> {
34 let client = CurrencyClient::from_env()?;
35 Ok(Self::new(client))
36 }
37
38 pub fn with_cache_config(
40 client: CurrencyClient,
41 cache_config: crate::cache::CacheConfig,
42 ) -> Self {
43 Self {
44 client,
45 cache: ExchangeRateCache::with_config(cache_config),
46 }
47 }
48
49 pub async fn get_latest_rates(&self, base_currency: &str) -> Result<ExchangeRateResponse> {
51 self.validate_currency_code(base_currency)?;
53
54 let cache_key = CacheKey::latest(base_currency);
55
56 if let Some(cached_rates) = self.cache.get(&cache_key).await {
58 return Ok(cached_rates);
59 }
60
61 let api_key = self.get_api_key()?;
65
66 let endpoint = format!("{}/latest/{}", api_key, base_currency.to_uppercase());
68
69 match self.client.get::<ExchangeRateResponse>(&endpoint).await {
71 Ok(response) => {
72 if response.is_success() {
73 self.cache.put(cache_key, response.clone()).await;
75 Ok(response)
76 } else {
77 Err(CurrencyError::api(format!(
78 "API returned unsuccessful result: {}",
79 response.result
80 )))
81 }
82 }
83 Err(e) => {
84 if let Ok(error_response) = self.try_parse_error_response(&endpoint).await {
86 return Err(CurrencyError::api(format!(
87 "API error: {} - {}",
88 error_response.error_type,
89 error_response.extra_info.unwrap_or_default()
90 )));
91 }
92 Err(e)
93 }
94 }
95 }
96
97 pub async fn get_historical_rates(
99 &self,
100 base_currency: &str,
101 date: &str,
102 ) -> Result<HistoricalRateResponse> {
103 self.validate_currency_code(base_currency)?;
105 self.validate_date_format(date)?;
106
107 let cache_key = CacheKey::historical(base_currency, date);
108
109 if let Some(cached_rates) = self.cache.get(&cache_key).await {
111 println!(
112 "๐ Using cached historical rates for {} on {}",
113 base_currency.to_uppercase(),
114 date
115 );
116 return Ok(cached_rates.to_historical(date));
117 }
118
119 println!(
121 "๐ Fetching historical rates for {} on {}",
122 base_currency.to_uppercase(),
123 date
124 );
125
126 let api_key = self.get_api_key()?;
127 let endpoint = format!(
128 "history/{}/{}/{}",
129 api_key,
130 base_currency.to_uppercase(),
131 date
132 );
133
134 match self.client.get::<HistoricalRateResponse>(&endpoint).await {
136 Ok(response) => {
137 if response.is_success() {
138 println!("โ
Successfully fetched historical rates for {}", date);
139
140 let standard_response = response.to_standard();
142 self.cache.put(cache_key, standard_response).await;
143
144 Ok(response)
145 } else {
146 Err(CurrencyError::api(format!(
147 "Historical data request failed: {}",
148 response.result
149 )))
150 }
151 }
152 Err(_) => {
153 match self.get_latest_rates(base_currency).await {
155 Ok(current_rates) => {
156 println!("โ ๏ธ Using current rates as historical fallback for {}", date);
157 Ok(current_rates.to_historical(date))
158 }
159 Err(e) => Err(e),
160 }
161 }
162 }
163 }
164
165 pub async fn convert_historical(
167 &self,
168 request: HistoricalConversionRequest,
169 ) -> Result<ConversionResponse> {
170 self.validate_currency_code(&request.from)?;
172 self.validate_currency_code(&request.to)?;
173 self.validate_amount(request.amount)?;
174 self.validate_date_format(&request.date)?;
175
176 println!(
177 "๐ฑ Historical conversion: {} {} to {} on {}",
178 request.amount,
179 request.from.to_uppercase(),
180 request.to.to_uppercase(),
181 request.date
182 );
183
184 let rates = self
186 .get_historical_rates(&request.from, &request.date)
187 .await?;
188
189 let rate = rates.get_rate(&request.to.to_uppercase()).ok_or_else(|| {
191 CurrencyError::api(format!(
192 "Historical rate not available for {} on {}",
193 request.to.to_uppercase(),
194 request.date
195 ))
196 })?;
197
198 let converted_amount = request.amount * rate;
199
200 println!(
201 "โ
Historical conversion: {} {} = {:.6} {} (rate: {:.6})",
202 request.amount,
203 request.from.to_uppercase(),
204 converted_amount,
205 request.to.to_uppercase(),
206 rate
207 );
208
209 Ok(ConversionResponse {
210 result: "success".to_string(),
211 base_code: request.from.to_uppercase(),
212 target_code: request.to.to_uppercase(),
213 conversion_rate: rate,
214 conversion_result: converted_amount,
215 })
216 }
217
218 pub async fn convert_currency(
220 &self,
221 from: &str,
222 to: &str,
223 amount: f64,
224 ) -> Result<ConversionResponse> {
225 self.validate_currency_code(from)?;
227 self.validate_currency_code(to)?;
228 self.validate_amount(amount)?;
229
230 let api_key = self.get_api_key()?;
231
232 let endpoint = format!(
234 "pair/{}/{}/{}/{}",
235 api_key,
236 from.to_uppercase(),
237 to.to_uppercase(),
238 amount
239 );
240
241 println!(
242 "๐ฑ Converting {} {} to {}",
243 amount,
244 from.to_uppercase(),
245 to.to_uppercase()
246 );
247
248 match self.client.get::<ConversionResponse>(&endpoint).await {
250 Ok(response) => {
251 if response.is_success() {
252 println!(
253 "โ
Conversion successful: {} {} = {} {}",
254 amount,
255 from.to_uppercase(),
256 response.conversion_result,
257 to.to_uppercase()
258 );
259 Ok(response)
260 } else {
261 Err(CurrencyError::api(format!(
262 "Conversion failed: {}",
263 response.result
264 )))
265 }
266 }
267 Err(e) => {
268 if let Ok(error_response) = self.try_parse_error_response(&endpoint).await {
269 return Err(CurrencyError::api(format!(
270 "Conversion error: {} - {}",
271 error_response.error_type,
272 error_response.extra_info.unwrap_or_default()
273 )));
274 }
275 Err(e)
276 }
277 }
278 }
279
280 pub async fn get_supported_currencies(&self) -> Result<SupportedCurrenciesResponse> {
282 let api_key = self.get_api_key()?;
283 let endpoint = format!("codes/{}", api_key);
284
285 println!("๐ Fetching supported currencies");
286
287 let response = self
288 .client
289 .get::<SupportedCurrenciesResponse>(&endpoint)
290 .await?;
291
292 if response.result == "success" {
293 println!(
294 "โ
Found {} supported currencies",
295 response.supported_codes.len()
296 );
297 Ok(response)
298 } else {
299 Err(CurrencyError::api(format!(
300 "Failed to fetch supported currencies: {}",
301 response.result
302 )))
303 }
304 }
305
306 pub async fn is_currency_supported(&self, currency: &str) -> Result<bool> {
308 match self.get_latest_rates(currency).await {
309 Ok(_) => Ok(true),
310 Err(CurrencyError::Api { message }) if message.contains("unsupported-code") => {
311 Ok(false)
312 }
313 Err(e) => Err(e),
314 }
315 }
316
317 pub async fn batch_convert(
319 &self,
320 base_currency: &str,
321 target_currencies: &[String],
322 amount: f64,
323 ) -> Result<Vec<Result<ConversionResponse>>> {
324 let rates = self.get_latest_rates(base_currency).await?;
326
327 println!(
328 "๐ Batch converting {} {} to {} currencies",
329 amount,
330 base_currency.to_uppercase(),
331 target_currencies.len()
332 );
333
334 let mut results = Vec::new();
335
336 for target in target_currencies {
337 let result = if let Some(rate) = rates.get_rate(&target.to_uppercase()) {
338 let converted_amount = amount * rate;
339 Ok(ConversionResponse {
340 result: "success".to_string(),
341 base_code: base_currency.to_uppercase(),
342 target_code: target.to_uppercase(),
343 conversion_rate: rate,
344 conversion_result: converted_amount,
345 })
346 } else {
347 Err(CurrencyError::api(format!(
348 "Exchange rate not available for {}",
349 target.to_uppercase()
350 )))
351 };
352 results.push(result);
353 }
354
355 println!("โ
Completed batch conversion");
356 Ok(results)
357 }
358
359 pub fn get_cache_stats(&self) -> crate::cache::CacheStats {
361 self.cache.get_stats()
362 }
363
364 pub async fn clear_cache(&self) {
366 self.cache.clear().await;
367 println!("๐๏ธ Cache cleared");
368 }
369
370 pub async fn cleanup_cache(&self) {
372 self.cache.cleanup_expired().await;
373 println!("๐งน Cache cleanup completed");
374 }
375
376 fn validate_currency_code(&self, code: &str) -> Result<()> {
378 if !is_valid_currency_code(code) {
379 return Err(CurrencyError::invalid_currency(format!(
380 "{} (must be 3 uppercase letters, e.g., USD, EUR, GBP)",
381 code
382 )));
383 }
384 Ok(())
385 }
386
387 fn validate_date_format(&self, date: &str) -> Result<()> {
389 if !is_valid_date_format(date) {
390 return Err(CurrencyError::conversion(format!(
391 "Invalid date format: '{}'. Use YYYY-MM-DD format (e.g., 2024-01-15)",
392 date
393 )));
394 }
395 Ok(())
396 }
397
398 fn validate_amount(&self, amount: f64) -> Result<()> {
400 if amount <= 0.0 {
401 return Err(CurrencyError::invalid_amount(amount));
402 }
403 if amount.is_nan() || amount.is_infinite() {
404 return Err(CurrencyError::invalid_amount(amount));
405 }
406 if amount > 1_000_000_000.0 {
408 return Err(CurrencyError::invalid_amount(amount));
409 }
410 Ok(())
411 }
412
413 fn get_api_key(&self) -> Result<String> {
415 if !self.client.has_api_key() {
416 return Err(CurrencyError::configuration(
417 "API key required. Set EXCHANGE_API_KEY environment variable",
418 ));
419 }
420 std::env::var("EXCHANGE_API_KEY")
423 .map_err(|_| CurrencyError::configuration("EXCHANGE_API_KEY not found"))
424 }
425
426 async fn try_parse_error_response(&self, endpoint: &str) -> Result<ApiErrorResponse> {
428 self.client.get::<ApiErrorResponse>(endpoint).await
431 }
432}