use rust_decimal::Decimal;
use std::collections::HashMap;
use crate::amount::CurrencyAmount;
use crate::currency::Currency;
use crate::error::{MoneyError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
#[cfg_attr(feature = "serde_impl", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct FxRate {
pub from: Currency,
pub to: Currency,
pub rate: Decimal,
}
impl FxRate {
pub fn new(from: Currency, to: Currency, rate: Decimal) -> Result<Self> {
if rate <= Decimal::ZERO {
return Err(MoneyError::InvalidAmount(
"exchange rate must be positive".into(),
));
}
Ok(Self { from, to, rate })
}
pub fn identity(currency: Currency) -> Self {
Self {
from: currency,
to: currency,
rate: Decimal::from(1),
}
}
pub fn inverse(&self) -> Self {
Self {
from: self.to,
to: self.from,
rate: Decimal::from(1) / self.rate,
}
}
}
pub trait FxProvider {
fn get_rate(&self, from: Currency, to: Currency) -> Result<FxRate>;
fn get_rates_from(&self, from: Currency) -> Result<Vec<FxRate>>;
}
pub struct InMemoryFxProvider {
rates: HashMap<(Currency, Currency), Decimal>,
}
impl InMemoryFxProvider {
pub fn new() -> Self {
Self {
rates: HashMap::new(),
}
}
pub fn set_rate(&mut self, from: Currency, to: Currency, rate: Decimal) -> Result<()> {
if rate <= Decimal::ZERO {
return Err(MoneyError::InvalidAmount(
"exchange rate must be positive".into(),
));
}
self.rates.insert((from, to), rate);
Ok(())
}
pub fn load_rates(&mut self, rates: &[FxRate]) {
for rate in rates {
self.rates.insert((rate.from, rate.to), rate.rate);
}
}
}
impl Default for InMemoryFxProvider {
fn default() -> Self {
Self::new()
}
}
impl FxProvider for InMemoryFxProvider {
fn get_rate(&self, from: Currency, to: Currency) -> Result<FxRate> {
if from == to {
return Ok(FxRate::identity(from));
}
let rate = self
.rates
.get(&(from, to))
.ok_or_else(|| MoneyError::InvalidAmount(format!("No rate for {from} -> {to}")))?;
FxRate::new(from, to, *rate)
}
fn get_rates_from(&self, from: Currency) -> Result<Vec<FxRate>> {
self.rates
.iter()
.filter(|((f, _), _)| *f == from)
.map(|((f, t), r)| FxRate::new(*f, *t, *r))
.collect()
}
}
pub fn convert(
amount: &CurrencyAmount,
to: Currency,
provider: &impl FxProvider,
) -> Result<CurrencyAmount> {
let rate = provider.get_rate(amount.currency, to)?;
Ok(CurrencyAmount::new(amount.amount * rate.rate, to))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fx_rate_inverse() {
let rate = FxRate::new(Currency::USD, Currency::EUR, Decimal::try_from("0.85").unwrap()).unwrap();
let inv = rate.inverse();
assert_eq!(inv.from, Currency::EUR);
assert_eq!(inv.to, Currency::USD);
}
#[test]
fn test_fx_rate_rejects_zero() {
assert!(FxRate::new(Currency::USD, Currency::EUR, Decimal::ZERO).is_err());
}
#[test]
fn test_fx_rate_rejects_negative() {
assert!(FxRate::new(Currency::USD, Currency::EUR, Decimal::try_from("-0.85").unwrap()).is_err());
}
#[test]
fn test_in_memory_provider() {
let mut provider = InMemoryFxProvider::new();
provider.set_rate(
Currency::USD,
Currency::EUR,
Decimal::try_from("0.85").unwrap(),
)
.unwrap();
let rate = provider.get_rate(Currency::USD, Currency::EUR).unwrap();
assert_eq!(rate.rate, Decimal::try_from("0.85").unwrap());
let identity = provider.get_rate(Currency::USD, Currency::USD).unwrap();
assert_eq!(identity.rate, Decimal::from(1));
}
}