Skip to main content

klirr_core/models/
exchange_rates.rs

1use std::ops::Mul;
2
3use crate::prelude::*;
4
5/// Represents exchange rates for a specific target currency in relation to other currencies.
6#[derive(Clone, Debug, Serialize, Builder, Getters)]
7pub struct ExchangeRates {
8    /// MUST Match the currency of the invoice, e.g. `"EUR"`.
9    #[getset(get = "pub")]
10    target_currency: Currency,
11
12    /// Exchange rates for the `target_currency` in relation to other currencies.
13    /// The keys are the base currencies, and the values are the exchange rates.
14    ///
15    /// For example, if the `target_currency` is `"EUR"` and the rates are:
16    /// ```text
17    /// "USD": 1.2,
18    /// "GBP": 0.85,
19    /// "SEK": 11.0
20    /// ```
21    /// then 1 USD is 1.2 EUR, 1 GBP is 0.85 EUR, and 1 SEK is 11.0 EUR.
22    ///
23    #[getset(get = "pub")]
24    rates: ExchangeRatesMap,
25}
26
27impl ExchangeRates {
28    /// Converts a given `unit_price` from the `currency` to the `target_currency`.
29    /// If the `currency` is the same as the `target_currency`, it returns the `unit_price
30    /// as is.
31    /// If the `currency` is not found in the exchange rates, it returns an error.
32    /// If the conversion is successful, it returns the converted `UnitPrice`.
33    ///
34    /// # Examples
35    /// ```
36    /// extern crate klirr_core;
37    /// use klirr_core::prelude::*;
38    /// let exchange_rates = ExchangeRates::builder()
39    ///     .target_currency(Currency::EUR)
40    ///     .rates(ExchangeRatesMap::from([
41    ///         (Currency::USD, UnitPrice::from(dec!(0.85))),
42    ///         (Currency::GBP, UnitPrice::from(dec!(1.1))),
43    ///     ]))
44    ///     .build();
45    /// let converted = exchange_rates.convert(dec!(100.0), Currency::USD).unwrap();
46    /// assert_eq!(*converted, dec!(85.0));
47    /// ```
48    ///
49    /// # Errors
50    /// Returns an error if the `currency` is not found in the exchange rates.
51    ///
52    pub fn convert(
53        &self,
54        unit_price: impl Into<UnitPrice>,
55        currency: Currency,
56    ) -> Result<UnitPrice> {
57        let unit_price = unit_price.into();
58        if self.target_currency == currency {
59            return Ok(unit_price);
60        }
61        let rate = self.get_rate(currency)?;
62        let converted = rate.mul(*unit_price);
63        Ok(converted)
64    }
65
66    fn get_rate(&self, currency: Currency) -> Result<UnitPrice> {
67        self.rates
68            .get(&currency)
69            .cloned()
70            .ok_or(Error::FoundNoExchangeRate {
71                target: self.target_currency,
72                base: currency,
73            })
74    }
75}
76
77impl ExchangeRates {
78    pub fn hard_coded() -> Self {
79        let rates = ExchangeRatesMap::from([
80            (Currency::EUR, UnitPrice::from(dec!(1.0))),
81            (Currency::USD, UnitPrice::from(dec!(1.2))),
82            (Currency::GBP, UnitPrice::from(dec!(0.85))),
83            (Currency::SEK, UnitPrice::from(dec!(11.0))),
84        ]);
85        Self {
86            target_currency: Currency::EUR,
87            rates,
88        }
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use test_log::test;
96
97    #[test]
98    fn test_hard_coded() {
99        let exchange_rates = ExchangeRates::hard_coded();
100        assert!(!exchange_rates.rates().is_empty());
101    }
102
103    #[test]
104    fn test_convert() {
105        let exchange_rates = ExchangeRates::hard_coded();
106        let converted = exchange_rates.convert(dec!(100.0), Currency::USD).unwrap();
107        assert_eq!(*converted, dec!(120.0));
108    }
109
110    #[test]
111    fn test_convert_not_found() {
112        let exchange_rates = ExchangeRates::builder()
113            .target_currency(Currency::EUR)
114            .rates(ExchangeRatesMap::new())
115            .build();
116        let result = exchange_rates.convert(dec!(100.0), Currency::JPY);
117        assert!(result.is_err());
118    }
119
120    #[test]
121    fn test_get_rate_not_found() {
122        let exchange_rates = ExchangeRates::builder()
123            .target_currency(Currency::EUR)
124            .rates(ExchangeRatesMap::new())
125            .build();
126        let result = exchange_rates.get_rate(Currency::JPY);
127        assert!(result.is_err());
128    }
129}