Skip to main content

stateset_embedded/
currency.rs

1//! Multi-currency operations
2//!
3//! Provides exchange rate management, currency conversion, and multi-currency
4//! support for international commerce.
5
6use rust_decimal::Decimal;
7use stateset_core::{
8    ConversionResult, ConvertCurrency, Currency, ExchangeRate, ExchangeRateFilter, Result,
9    SetExchangeRate, StoreCurrencySettings,
10};
11use stateset_db::Database;
12use std::sync::Arc;
13use uuid::Uuid;
14
15/// Currency operations interface.
16///
17/// Provides exchange rate management and currency conversion for
18/// multi-currency commerce operations.
19///
20/// # Example
21///
22/// ```rust,no_run
23/// use stateset_embedded::{Commerce, Currency, ConvertCurrency};
24/// use rust_decimal_macros::dec;
25///
26/// let commerce = Commerce::new("./store.db")?;
27///
28/// // Get exchange rate
29/// if let Some(rate) = commerce.currency().get_rate(Currency::USD, Currency::EUR)? {
30///     println!("USD to EUR: {}", rate.rate);
31/// }
32///
33/// // Convert currency
34/// let result = commerce.currency().convert(ConvertCurrency {
35///     from: Currency::USD,
36///     to: Currency::EUR,
37///     amount: dec!(100.00),
38/// })?;
39/// println!("$100 USD = €{} EUR", result.converted_amount);
40/// # Ok::<(), stateset_embedded::CommerceError>(())
41/// ```
42pub struct CurrencyOps {
43    db: Arc<dyn Database>,
44}
45
46impl std::fmt::Debug for CurrencyOps {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("CurrencyOps").finish_non_exhaustive()
49    }
50}
51
52impl CurrencyOps {
53    pub(crate) fn new(db: Arc<dyn Database>) -> Self {
54        Self { db }
55    }
56
57    // ========================================================================
58    // Exchange Rate Operations
59    // ========================================================================
60
61    /// Get exchange rate between two currencies.
62    ///
63    /// Returns the current rate to convert from the base currency to the quote
64    /// currency. If no direct rate exists, attempts to find an inverse rate.
65    ///
66    /// # Example
67    ///
68    /// ```rust,no_run
69    /// # use stateset_embedded::*;
70    /// # let commerce = Commerce::new(":memory:")?;
71    /// // Get USD to EUR rate
72    /// if let Some(rate) = commerce.currency().get_rate(Currency::USD, Currency::EUR)? {
73    ///     println!("1 USD = {} EUR", rate.rate);
74    ///     println!("Rate source: {}", rate.source);
75    /// } else {
76    ///     println!("No rate found for USD/EUR");
77    /// }
78    /// # Ok::<(), CommerceError>(())
79    /// ```
80    pub fn get_rate(&self, from: Currency, to: Currency) -> Result<Option<ExchangeRate>> {
81        self.db.currency().get_rate(from, to)
82    }
83
84    /// Get all exchange rates for a base currency.
85    ///
86    /// # Example
87    ///
88    /// ```rust,no_run
89    /// # use stateset_embedded::*;
90    /// # let commerce = Commerce::new(":memory:")?;
91    /// // Get all rates from USD
92    /// let rates = commerce.currency().get_rates_for(Currency::USD)?;
93    /// for rate in rates {
94    ///     println!("1 USD = {} {}", rate.rate, rate.quote_currency);
95    /// }
96    /// # Ok::<(), CommerceError>(())
97    /// ```
98    pub fn get_rates_for(&self, base: Currency) -> Result<Vec<ExchangeRate>> {
99        self.db.currency().get_rates_for(base)
100    }
101
102    /// List exchange rates with optional filtering.
103    ///
104    /// # Example
105    ///
106    /// ```rust,no_run
107    /// # use stateset_embedded::*;
108    /// # let commerce = Commerce::new(":memory:")?;
109    /// // List all rates
110    /// let rates = commerce.currency().list_rates(ExchangeRateFilter::default())?;
111    ///
112    /// // Filter by base currency
113    /// let usd_rates = commerce.currency().list_rates(ExchangeRateFilter {
114    ///     base_currency: Some(Currency::USD),
115    ///     ..Default::default()
116    /// })?;
117    /// # Ok::<(), CommerceError>(())
118    /// ```
119    pub fn list_rates(&self, filter: ExchangeRateFilter) -> Result<Vec<ExchangeRate>> {
120        self.db.currency().list_rates(filter)
121    }
122
123    /// Set an exchange rate.
124    ///
125    /// Creates or updates the exchange rate between two currencies.
126    /// The rate is also recorded in history for auditing.
127    ///
128    /// # Example
129    ///
130    /// ```rust,no_run
131    /// # use stateset_embedded::*;
132    /// use rust_decimal_macros::dec;
133    ///
134    /// # let commerce = Commerce::new(":memory:")?;
135    /// // Set USD to EUR rate
136    /// let rate = commerce.currency().set_rate(SetExchangeRate {
137    ///     base_currency: Currency::USD,
138    ///     quote_currency: Currency::EUR,
139    ///     rate: dec!(0.92),
140    ///     source: Some("manual".into()),
141    /// })?;
142    ///
143    /// println!("Rate set: 1 USD = {} EUR", rate.rate);
144    /// # Ok::<(), CommerceError>(())
145    /// ```
146    pub fn set_rate(&self, input: SetExchangeRate) -> Result<ExchangeRate> {
147        self.db.currency().set_rate(input)
148    }
149
150    /// Set multiple exchange rates at once.
151    ///
152    /// Useful for bulk updates from external rate providers.
153    ///
154    /// # Example
155    ///
156    /// ```rust,no_run
157    /// # use stateset_embedded::*;
158    /// use rust_decimal_macros::dec;
159    ///
160    /// # let commerce = Commerce::new(":memory:")?;
161    /// let rates = commerce.currency().set_rates(vec![
162    ///     SetExchangeRate {
163    ///         base_currency: Currency::USD,
164    ///         quote_currency: Currency::EUR,
165    ///         rate: dec!(0.92),
166    ///         source: Some("api".into()),
167    ///     },
168    ///     SetExchangeRate {
169    ///         base_currency: Currency::USD,
170    ///         quote_currency: Currency::GBP,
171    ///         rate: dec!(0.79),
172    ///         source: Some("api".into()),
173    ///     },
174    /// ])?;
175    ///
176    /// println!("Updated {} exchange rates", rates.len());
177    /// # Ok::<(), CommerceError>(())
178    /// ```
179    pub fn set_rates(&self, rates: Vec<SetExchangeRate>) -> Result<Vec<ExchangeRate>> {
180        self.db.currency().set_rates(rates)
181    }
182
183    /// Delete an exchange rate by ID.
184    ///
185    /// # Example
186    ///
187    /// ```rust,no_run
188    /// # use stateset_embedded::*;
189    /// # use uuid::Uuid;
190    /// # let commerce = Commerce::new(":memory:")?;
191    /// # let rate_id = Uuid::new_v4();
192    /// commerce.currency().delete_rate(rate_id)?;
193    /// # Ok::<(), CommerceError>(())
194    /// ```
195    pub fn delete_rate(&self, id: Uuid) -> Result<()> {
196        self.db.currency().delete_rate(id)
197    }
198
199    // ========================================================================
200    // Currency Conversion
201    // ========================================================================
202
203    /// Convert an amount from one currency to another.
204    ///
205    /// Uses the current exchange rate to convert the amount. If the rate
206    /// is not found directly, attempts to use the inverse rate.
207    ///
208    /// # Example
209    ///
210    /// ```rust,no_run
211    /// # use stateset_embedded::*;
212    /// use rust_decimal_macros::dec;
213    ///
214    /// # let commerce = Commerce::new(":memory:")?;
215    /// let result = commerce.currency().convert(ConvertCurrency {
216    ///     from: Currency::USD,
217    ///     to: Currency::EUR,
218    ///     amount: dec!(100.00),
219    /// })?;
220    ///
221    /// println!("${} USD = €{} EUR", result.original_amount, result.converted_amount);
222    /// println!("Rate used: {} (at {})", result.rate, result.rate_at);
223    /// # Ok::<(), CommerceError>(())
224    /// ```
225    pub fn convert(&self, input: ConvertCurrency) -> Result<ConversionResult> {
226        self.db.currency().convert(input)
227    }
228
229    /// Convert an amount between currencies (convenience method).
230    ///
231    /// # Example
232    ///
233    /// ```rust,no_run
234    /// # use stateset_embedded::*;
235    /// use rust_decimal_macros::dec;
236    ///
237    /// # let commerce = Commerce::new(":memory:")?;
238    /// let eur_amount = commerce.currency().convert_amount(
239    ///     dec!(100.00),
240    ///     Currency::USD,
241    ///     Currency::EUR
242    /// )?;
243    ///
244    /// println!("€{}", eur_amount);
245    /// # Ok::<(), CommerceError>(())
246    /// ```
247    pub fn convert_amount(&self, amount: Decimal, from: Currency, to: Currency) -> Result<Decimal> {
248        let result = self.convert(ConvertCurrency { from, to, amount })?;
249        Ok(result.converted_amount)
250    }
251
252    // ========================================================================
253    // Store Currency Settings
254    // ========================================================================
255
256    /// Get store currency settings.
257    ///
258    /// Returns the base currency, enabled currencies, and conversion settings
259    /// for the store.
260    ///
261    /// # Example
262    ///
263    /// ```rust,no_run
264    /// # use stateset_embedded::*;
265    /// # let commerce = Commerce::new(":memory:")?;
266    /// let settings = commerce.currency().get_settings()?;
267    ///
268    /// println!("Base currency: {}", settings.base_currency);
269    /// println!("Enabled currencies: {:?}", settings.enabled_currencies);
270    /// println!("Auto-convert: {}", settings.auto_convert);
271    /// println!("Rounding: {:?}", settings.rounding_mode);
272    /// # Ok::<(), CommerceError>(())
273    /// ```
274    pub fn get_settings(&self) -> Result<StoreCurrencySettings> {
275        self.db.currency().get_settings()
276    }
277
278    /// Update store currency settings.
279    ///
280    /// # Example
281    ///
282    /// ```rust,no_run
283    /// # use stateset_embedded::*;
284    /// # let commerce = Commerce::new(":memory:")?;
285    /// let settings = commerce.currency().update_settings(StoreCurrencySettings {
286    ///     base_currency: Currency::EUR,
287    ///     enabled_currencies: vec![Currency::EUR, Currency::USD, Currency::GBP],
288    ///     auto_convert: true,
289    ///     rounding_mode: RoundingMode::HalfUp,
290    /// })?;
291    ///
292    /// println!("Updated base currency to: {}", settings.base_currency);
293    /// # Ok::<(), CommerceError>(())
294    /// ```
295    pub fn update_settings(
296        &self,
297        settings: StoreCurrencySettings,
298    ) -> Result<StoreCurrencySettings> {
299        self.db.currency().update_settings(settings)
300    }
301
302    /// Set the store's base currency.
303    ///
304    /// Convenience method to update just the base currency.
305    ///
306    /// # Example
307    ///
308    /// ```rust,no_run
309    /// # use stateset_embedded::*;
310    /// # let commerce = Commerce::new(":memory:")?;
311    /// commerce.currency().set_base_currency(Currency::EUR)?;
312    /// # Ok::<(), CommerceError>(())
313    /// ```
314    pub fn set_base_currency(&self, currency: Currency) -> Result<StoreCurrencySettings> {
315        let mut settings = self.get_settings()?;
316        settings.base_currency = currency;
317        self.update_settings(settings)
318    }
319
320    /// Enable currencies for the store.
321    ///
322    /// # Example
323    ///
324    /// ```rust,no_run
325    /// # use stateset_embedded::*;
326    /// # let commerce = Commerce::new(":memory:")?;
327    /// commerce.currency().enable_currencies(vec![
328    ///     Currency::USD,
329    ///     Currency::EUR,
330    ///     Currency::GBP,
331    ///     Currency::JPY,
332    /// ])?;
333    /// # Ok::<(), CommerceError>(())
334    /// ```
335    pub fn enable_currencies(&self, currencies: Vec<Currency>) -> Result<StoreCurrencySettings> {
336        let mut settings = self.get_settings()?;
337        settings.enabled_currencies = currencies;
338        self.update_settings(settings)
339    }
340
341    /// Check if a currency is enabled for the store.
342    ///
343    /// # Example
344    ///
345    /// ```rust,no_run
346    /// # use stateset_embedded::*;
347    /// # let commerce = Commerce::new(":memory:")?;
348    /// if commerce.currency().is_enabled(Currency::EUR)? {
349    ///     println!("EUR is enabled");
350    /// }
351    /// # Ok::<(), CommerceError>(())
352    /// ```
353    pub fn is_enabled(&self, currency: Currency) -> Result<bool> {
354        let settings = self.get_settings()?;
355        Ok(settings.enabled_currencies.contains(&currency))
356    }
357
358    // ========================================================================
359    // Utility Methods
360    // ========================================================================
361
362    /// Get the store's base currency.
363    ///
364    /// # Example
365    ///
366    /// ```rust,no_run
367    /// # use stateset_embedded::*;
368    /// # let commerce = Commerce::new(":memory:")?;
369    /// let base = commerce.currency().base_currency()?;
370    /// println!("Store base currency: {}", base);
371    /// # Ok::<(), CommerceError>(())
372    /// ```
373    pub fn base_currency(&self) -> Result<Currency> {
374        let settings = self.get_settings()?;
375        Ok(settings.base_currency)
376    }
377
378    /// Get all enabled currencies for the store.
379    ///
380    /// # Example
381    ///
382    /// ```rust,no_run
383    /// # use stateset_embedded::*;
384    /// # let commerce = Commerce::new(":memory:")?;
385    /// let currencies = commerce.currency().enabled_currencies()?;
386    /// for c in currencies {
387    ///     println!("Enabled: {} ({})", c.name(), c.code());
388    /// }
389    /// # Ok::<(), CommerceError>(())
390    /// ```
391    pub fn enabled_currencies(&self) -> Result<Vec<Currency>> {
392        let settings = self.get_settings()?;
393        Ok(settings.enabled_currencies)
394    }
395
396    /// Format an amount with currency symbol.
397    ///
398    /// # Example
399    ///
400    /// ```rust,no_run
401    /// # use stateset_embedded::*;
402    /// use rust_decimal_macros::dec;
403    ///
404    /// # let commerce = Commerce::new(":memory:")?;
405    /// let formatted = commerce.currency().format(dec!(99.99), Currency::USD);
406    /// println!("{}", formatted); // "$99.99"
407    /// # Ok::<(), CommerceError>(())
408    /// ```
409    #[must_use]
410    pub fn format(&self, amount: Decimal, currency: Currency) -> String {
411        format!("{}{}", currency.symbol(), amount)
412    }
413}