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(¤cy))
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}