use crate::{
CurrencyError, Result,
api::{
client::CurrencyClient,
types::{
ApiErrorResponse, ConversionResponse, ExchangeRateResponse,
HistoricalConversionRequest, HistoricalRateResponse, SupportedCurrenciesResponse,
is_valid_currency_code, is_valid_date_format,
},
},
cache::{CacheKey, ExchangeRateCache},
};
#[derive(Debug, Clone)]
pub struct ExchangeRateService {
client: CurrencyClient,
cache: ExchangeRateCache,
}
impl ExchangeRateService {
pub fn new(client: CurrencyClient) -> Self {
Self {
client,
cache: ExchangeRateCache::new(),
}
}
pub fn from_env() -> Result<Self> {
let client = CurrencyClient::from_env()?;
Ok(Self::new(client))
}
pub fn with_cache_config(
client: CurrencyClient,
cache_config: crate::cache::CacheConfig,
) -> Self {
Self {
client,
cache: ExchangeRateCache::with_config(cache_config),
}
}
pub async fn get_latest_rates(&self, base_currency: &str) -> Result<ExchangeRateResponse> {
self.validate_currency_code(base_currency)?;
let cache_key = CacheKey::latest(base_currency);
if let Some(cached_rates) = self.cache.get(&cache_key).await {
return Ok(cached_rates);
}
let api_key = self.get_api_key()?;
let endpoint = format!("{}/latest/{}", api_key, base_currency.to_uppercase());
match self.client.get::<ExchangeRateResponse>(&endpoint).await {
Ok(response) => {
if response.is_success() {
self.cache.put(cache_key, response.clone()).await;
Ok(response)
} else {
Err(CurrencyError::api(format!(
"API returned unsuccessful result: {}",
response.result
)))
}
}
Err(e) => {
if let Ok(error_response) = self.try_parse_error_response(&endpoint).await {
return Err(CurrencyError::api(format!(
"API error: {} - {}",
error_response.error_type,
error_response.extra_info.unwrap_or_default()
)));
}
Err(e)
}
}
}
pub async fn get_historical_rates(
&self,
base_currency: &str,
date: &str,
) -> Result<HistoricalRateResponse> {
self.validate_currency_code(base_currency)?;
self.validate_date_format(date)?;
let cache_key = CacheKey::historical(base_currency, date);
if let Some(cached_rates) = self.cache.get(&cache_key).await {
println!(
"๐ Using cached historical rates for {} on {}",
base_currency.to_uppercase(),
date
);
return Ok(cached_rates.to_historical(date));
}
println!(
"๐ Fetching historical rates for {} on {}",
base_currency.to_uppercase(),
date
);
let api_key = self.get_api_key()?;
let endpoint = format!(
"history/{}/{}/{}",
api_key,
base_currency.to_uppercase(),
date
);
match self.client.get::<HistoricalRateResponse>(&endpoint).await {
Ok(response) => {
if response.is_success() {
println!("โ
Successfully fetched historical rates for {}", date);
let standard_response = response.to_standard();
self.cache.put(cache_key, standard_response).await;
Ok(response)
} else {
Err(CurrencyError::api(format!(
"Historical data request failed: {}",
response.result
)))
}
}
Err(_) => {
match self.get_latest_rates(base_currency).await {
Ok(current_rates) => {
println!("โ ๏ธ Using current rates as historical fallback for {}", date);
Ok(current_rates.to_historical(date))
}
Err(e) => Err(e),
}
}
}
}
pub async fn convert_historical(
&self,
request: HistoricalConversionRequest,
) -> Result<ConversionResponse> {
self.validate_currency_code(&request.from)?;
self.validate_currency_code(&request.to)?;
self.validate_amount(request.amount)?;
self.validate_date_format(&request.date)?;
println!(
"๐ฑ Historical conversion: {} {} to {} on {}",
request.amount,
request.from.to_uppercase(),
request.to.to_uppercase(),
request.date
);
let rates = self
.get_historical_rates(&request.from, &request.date)
.await?;
let rate = rates.get_rate(&request.to.to_uppercase()).ok_or_else(|| {
CurrencyError::api(format!(
"Historical rate not available for {} on {}",
request.to.to_uppercase(),
request.date
))
})?;
let converted_amount = request.amount * rate;
println!(
"โ
Historical conversion: {} {} = {:.6} {} (rate: {:.6})",
request.amount,
request.from.to_uppercase(),
converted_amount,
request.to.to_uppercase(),
rate
);
Ok(ConversionResponse {
result: "success".to_string(),
base_code: request.from.to_uppercase(),
target_code: request.to.to_uppercase(),
conversion_rate: rate,
conversion_result: converted_amount,
})
}
pub async fn convert_currency(
&self,
from: &str,
to: &str,
amount: f64,
) -> Result<ConversionResponse> {
self.validate_currency_code(from)?;
self.validate_currency_code(to)?;
self.validate_amount(amount)?;
let api_key = self.get_api_key()?;
let endpoint = format!(
"pair/{}/{}/{}/{}",
api_key,
from.to_uppercase(),
to.to_uppercase(),
amount
);
println!(
"๐ฑ Converting {} {} to {}",
amount,
from.to_uppercase(),
to.to_uppercase()
);
match self.client.get::<ConversionResponse>(&endpoint).await {
Ok(response) => {
if response.is_success() {
println!(
"โ
Conversion successful: {} {} = {} {}",
amount,
from.to_uppercase(),
response.conversion_result,
to.to_uppercase()
);
Ok(response)
} else {
Err(CurrencyError::api(format!(
"Conversion failed: {}",
response.result
)))
}
}
Err(e) => {
if let Ok(error_response) = self.try_parse_error_response(&endpoint).await {
return Err(CurrencyError::api(format!(
"Conversion error: {} - {}",
error_response.error_type,
error_response.extra_info.unwrap_or_default()
)));
}
Err(e)
}
}
}
pub async fn get_supported_currencies(&self) -> Result<SupportedCurrenciesResponse> {
let api_key = self.get_api_key()?;
let endpoint = format!("codes/{}", api_key);
println!("๐ Fetching supported currencies");
let response = self
.client
.get::<SupportedCurrenciesResponse>(&endpoint)
.await?;
if response.result == "success" {
println!(
"โ
Found {} supported currencies",
response.supported_codes.len()
);
Ok(response)
} else {
Err(CurrencyError::api(format!(
"Failed to fetch supported currencies: {}",
response.result
)))
}
}
pub async fn is_currency_supported(&self, currency: &str) -> Result<bool> {
match self.get_latest_rates(currency).await {
Ok(_) => Ok(true),
Err(CurrencyError::Api { message }) if message.contains("unsupported-code") => {
Ok(false)
}
Err(e) => Err(e),
}
}
pub async fn batch_convert(
&self,
base_currency: &str,
target_currencies: &[String],
amount: f64,
) -> Result<Vec<Result<ConversionResponse>>> {
let rates = self.get_latest_rates(base_currency).await?;
println!(
"๐ Batch converting {} {} to {} currencies",
amount,
base_currency.to_uppercase(),
target_currencies.len()
);
let mut results = Vec::new();
for target in target_currencies {
let result = if let Some(rate) = rates.get_rate(&target.to_uppercase()) {
let converted_amount = amount * rate;
Ok(ConversionResponse {
result: "success".to_string(),
base_code: base_currency.to_uppercase(),
target_code: target.to_uppercase(),
conversion_rate: rate,
conversion_result: converted_amount,
})
} else {
Err(CurrencyError::api(format!(
"Exchange rate not available for {}",
target.to_uppercase()
)))
};
results.push(result);
}
println!("โ
Completed batch conversion");
Ok(results)
}
pub fn get_cache_stats(&self) -> crate::cache::CacheStats {
self.cache.get_stats()
}
pub async fn clear_cache(&self) {
self.cache.clear().await;
println!("๐๏ธ Cache cleared");
}
pub async fn cleanup_cache(&self) {
self.cache.cleanup_expired().await;
println!("๐งน Cache cleanup completed");
}
fn validate_currency_code(&self, code: &str) -> Result<()> {
if !is_valid_currency_code(code) {
return Err(CurrencyError::invalid_currency(format!(
"{} (must be 3 uppercase letters, e.g., USD, EUR, GBP)",
code
)));
}
Ok(())
}
fn validate_date_format(&self, date: &str) -> Result<()> {
if !is_valid_date_format(date) {
return Err(CurrencyError::conversion(format!(
"Invalid date format: '{}'. Use YYYY-MM-DD format (e.g., 2024-01-15)",
date
)));
}
Ok(())
}
fn validate_amount(&self, amount: f64) -> Result<()> {
if amount <= 0.0 {
return Err(CurrencyError::invalid_amount(amount));
}
if amount.is_nan() || amount.is_infinite() {
return Err(CurrencyError::invalid_amount(amount));
}
if amount > 1_000_000_000.0 {
return Err(CurrencyError::invalid_amount(amount));
}
Ok(())
}
fn get_api_key(&self) -> Result<String> {
if !self.client.has_api_key() {
return Err(CurrencyError::configuration(
"API key required. Set EXCHANGE_API_KEY environment variable",
));
}
std::env::var("EXCHANGE_API_KEY")
.map_err(|_| CurrencyError::configuration("EXCHANGE_API_KEY not found"))
}
async fn try_parse_error_response(&self, endpoint: &str) -> Result<ApiErrorResponse> {
self.client.get::<ApiErrorResponse>(endpoint).await
}
}