use crate::models::currency::{Currency, Price}; use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::collections::HashMap; use chrono::Utc; use actix_session::Session; /// Service for handling currency operations and conversions #[derive(Clone)] pub struct CurrencyService { exchange_rates_cache: HashMap, last_update: chrono::DateTime, default_display_currency: String, } impl CurrencyService { pub fn new() -> Self { let mut service = Self { exchange_rates_cache: HashMap::default(), last_update: Utc::now(), default_display_currency: "MC".to_string(), }; // MC is now the base currency - no conversion needed service.exchange_rates_cache.insert("MC".to_string(), dec!(1.0)); service.update_exchange_rates(); service } pub fn new_with_config( _cache_duration_minutes: u64, _base_currency: String, auto_update: bool, fallback_rates: HashMap, ) -> Self { let mut service = Self { exchange_rates_cache: fallback_rates, last_update: Utc::now(), default_display_currency: "MC".to_string(), }; if auto_update { service.update_exchange_rates(); } service } pub fn new_with_display_config( _cache_duration_minutes: u64, _base_currency: String, display_currency: String, auto_update: bool, fallback_rates: HashMap, ) -> Self { let mut service = Self { exchange_rates_cache: fallback_rates, last_update: Utc::now(), default_display_currency: display_currency, }; if auto_update { service.update_exchange_rates(); } service } pub fn builder() -> crate::models::builders::CurrencyServiceBuilder { crate::models::builders::CurrencyServiceBuilder::new() } /// Get all supported currencies pub fn get_supported_currencies(&self) -> Vec { // Return standard supported currencies with MC as base currency vec![ Currency { code: "MC".to_string(), name: "Mycelium Credit".to_string(), symbol: "MC".to_string(), currency_type: crate::models::currency::CurrencyType::Custom("credits".to_string()), exchange_rate_to_base: dec!(1.0), // MC is the base currency is_base_currency: true, decimal_places: 2, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), }, Currency { code: "USD".to_string(), name: "US Dollar".to_string(), symbol: "$".to_string(), currency_type: crate::models::currency::CurrencyType::Fiat, exchange_rate_to_base: dec!(1.0), // 1 USD = 1 MC (parity for UX preservation) is_base_currency: false, decimal_places: 2, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), }, Currency { code: "EUR".to_string(), name: "Euro".to_string(), symbol: "€".to_string(), currency_type: crate::models::currency::CurrencyType::Fiat, exchange_rate_to_base: dec!(0.85), // 1 EUR = 0.85 MC is_base_currency: false, decimal_places: 2, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), }, Currency { code: "CAD".to_string(), name: "Canadian Dollar".to_string(), symbol: "C$".to_string(), currency_type: crate::models::currency::CurrencyType::Fiat, exchange_rate_to_base: dec!(1.35), // 1 CAD = 1.35 MC is_base_currency: false, decimal_places: 2, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), }, Currency { code: "TFT".to_string(), name: "Mycelium Token".to_string(), symbol: "TFT".to_string(), currency_type: crate::models::currency::CurrencyType::Token, exchange_rate_to_base: dec!(0.05), // 1 TFT = 0.05 MC is_base_currency: false, decimal_places: 3, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), }, Currency { code: "AED".to_string(), name: "UAE Dirham".to_string(), symbol: "د.إ".to_string(), currency_type: crate::models::currency::CurrencyType::Fiat, exchange_rate_to_base: dec!(3.67), // 1 AED = 3.67 MC is_base_currency: false, decimal_places: 2, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), }, ] } /// Get currency by code pub fn get_currency(&self, code: &str) -> Option { self.get_supported_currencies() .into_iter() .find(|c| c.code == code) } /// Get base currency pub fn get_base_currency(&self) -> Currency { Currency { code: "MC".to_string(), name: "Mycelium Credit".to_string(), symbol: "MC".to_string(), currency_type: crate::models::currency::CurrencyType::Custom("credits".to_string()), exchange_rate_to_base: dec!(1.0), is_base_currency: true, decimal_places: 2, is_active: true, provider_config: None, last_updated: chrono::Utc::now(), } } /// Convert amount from one currency to another pub fn convert_amount( &self, amount: Decimal, from_currency: &str, to_currency: &str, ) -> Result { if from_currency == to_currency { return Ok(amount); } let base_currency_code = "MC"; // Use MC as base currency // Convert to base currency first if needed let base_amount = if from_currency == base_currency_code { amount } else { let from_rate = self.get_exchange_rate_to_base(from_currency)?; amount / from_rate }; // Convert from base currency to target currency if to_currency == base_currency_code { Ok(base_amount) } else { let to_rate = self.get_exchange_rate_to_base(to_currency)?; Ok(base_amount * to_rate) } } /// Get exchange rate from base currency to target currency pub fn get_exchange_rate_to_base(&self, currency_code: &str) -> Result { let base_currency_code = "MC"; // Use MC as base currency if currency_code == base_currency_code { return Ok(dec!(1.0)); } if let Some(rate) = self.exchange_rates_cache.get(currency_code) { Ok(*rate) } else if let Some(currency) = self.get_currency(currency_code) { Ok(currency.exchange_rate_to_base) } else { Err(format!("Currency {} not found", currency_code)) } } /// Create a Price object with conversion pub fn create_price( &self, base_amount: Decimal, base_currency: &str, display_currency: &str, ) -> Result { let conversion_rate = if base_currency == display_currency { dec!(1.0) } else { self.convert_amount(dec!(1.0), base_currency, display_currency)? }; let mut price = Price::new( base_amount, base_currency.to_string(), display_currency.to_string(), conversion_rate, ); // Update formatted display with proper currency symbol let formatted_display = self.format_price(price.display_amount, display_currency)?; price.update_formatted_display(formatted_display); Ok(price) } /// Format price with currency symbol pub fn format_price( &self, amount: Decimal, currency_code: &str, ) -> Result { if let Some(currency) = self.get_currency(currency_code) { Ok(currency.format_amount(amount)) } else { Err(format!("Currency {} not found", currency_code)) } } /// Update exchange rates with standard rates pub fn update_exchange_rates(&mut self) { // Use standard exchange rates without mock data let currencies = self.get_supported_currencies(); for currency in currencies { if !currency.is_base_currency { // Use the currency's exchange rate directly let new_rate = currency.exchange_rate_to_base; self.exchange_rates_cache.insert(currency.code.clone(), new_rate); } } self.last_update = Utc::now(); } /// Get the default display currency for this service instance pub fn get_default_display_currency(&self) -> &str { &self.default_display_currency } /// Get user's preferred currency from session or persistent data pub fn get_user_preferred_currency(&self, session: &Session) -> String { // First check session for temporary preference if let Ok(Some(currency)) = session.get::("preferred_currency") { return currency; } // Then check persistent user data if let Ok(Some(user_email)) = session.get::("user_email") { if let Some(persistent_data) = crate::services::user_persistence::UserPersistence::load_user_data(&user_email) { if let Some(display_currency) = persistent_data.display_currency { return display_currency; } } } // Fall back to service default, then system default if !self.default_display_currency.is_empty() { self.default_display_currency.clone() } else { "MC".to_string() // Default to MC when no preference is set } } /// Set user's preferred currency in session and persistent data pub fn set_user_preferred_currency(&self, session: &Session, currency_code: String) -> Result<(), String> { if self.get_currency(¤cy_code).is_none() { return Err(format!("Currency {} is not supported", currency_code)); } // Set in session for immediate use session.insert("preferred_currency", currency_code.clone()) .map_err(|e| format!("Failed to set currency preference in session: {}", e))?; // Save to persistent data if user is logged in if let Ok(Some(user_email)) = session.get::("user_email") { let mut persistent_data = crate::services::user_persistence::UserPersistence::load_user_data(&user_email) .unwrap_or_else(|| crate::services::user_persistence::UserPersistentData { user_email: user_email.clone(), ..Default::default() }); persistent_data.display_currency = Some(currency_code); if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data(&persistent_data) { // Don't fail the operation, session preference is still set } } Ok(()) } /// Get all exchange rates relative to base currency pub fn get_all_exchange_rates(&self) -> HashMap { let mut rates = HashMap::default(); let base_currency_code = "MC".to_string(); // Use MC as base currency // Base currency always has rate 1.0 rates.insert(base_currency_code.clone(), dec!(1.0)); // Add cached rates for (currency, rate) in &self.exchange_rates_cache { rates.insert(currency.clone(), *rate); } // Add any missing currencies from static config for currency in self.get_supported_currencies() { if !rates.contains_key(¤cy.code) && !currency.is_base_currency { rates.insert(currency.code.clone(), currency.exchange_rate_to_base); } } rates } /// Check if exchange rates need updating pub fn should_update_rates(&self) -> bool { // Check if rates need updating based on time interval (15 minutes default) let update_interval = chrono::Duration::minutes(15); Utc::now().signed_duration_since(self.last_update) > update_interval } /// Get currency statistics for admin/debug purposes pub fn get_currency_stats(&self) -> HashMap { let mut stats = HashMap::default(); stats.insert("total_currencies".to_string(), serde_json::Value::Number(serde_json::Number::from(self.get_supported_currencies().len()))); stats.insert("base_currency".to_string(), serde_json::Value::String("MC".to_string())); stats.insert("last_update".to_string(), serde_json::Value::String(self.last_update.to_rfc3339())); stats.insert("cached_rates_count".to_string(), serde_json::Value::Number(serde_json::Number::from(self.exchange_rates_cache.len()))); let rate_values: Vec = self.exchange_rates_cache.iter() .map(|(currency, rate)| { serde_json::json!({ "currency": currency, "rate": rate.to_string() }) }) .collect(); stats.insert("current_rates".to_string(), serde_json::Value::Array(rate_values)); stats } /// Convert product prices for display in user's preferred currency pub fn convert_product_prices( &self, products: &[crate::models::product::Product], display_currency: &str, ) -> Result, String> { let mut converted_prices = Vec::default(); for product in products { let price = self.create_price( product.base_price, &product.base_currency, display_currency, )?; converted_prices.push((product.id.clone(), price)); } Ok(converted_prices) } /// Get currency display info for frontend pub fn get_currency_display_info(&self) -> Vec { self.get_supported_currencies().iter() .filter(|c| c.is_active) .map(|currency| { serde_json::json!({ "code": currency.code, "name": currency.name, "symbol": currency.symbol, "type": currency.currency_type, "decimal_places": currency.decimal_places, "is_base": currency.is_base_currency }) }) .collect() } /// Convert USD amount to user's preferred display currency pub fn convert_usd_to_display_currency( &self, usd_amount: Decimal, session: &Session, ) -> Result<(Decimal, String), String> { let display_currency = self.get_user_preferred_currency(session); if display_currency == "USD" { Ok((usd_amount, "USD".to_string())) } else { let converted_amount = self.convert_amount(usd_amount, "USD", &display_currency)?; Ok((converted_amount, display_currency)) } } /// Convert user's display currency amount to USD pub fn convert_display_currency_to_usd( &self, amount: Decimal, session: &Session, ) -> Result { let display_currency = self.get_user_preferred_currency(session); if display_currency == "USD" { Ok(amount) } else { self.convert_amount(amount, &display_currency, "USD") } } /// Get formatted price in user's preferred currency pub fn get_formatted_price_for_user( &self, usd_amount: Decimal, session: &Session, ) -> Result { let (display_amount, display_currency) = self.convert_usd_to_display_currency(usd_amount, session)?; self.format_price(display_amount, &display_currency) } /// Get quick top-up suggestions based on user's preferred currency pub fn get_suggested_topup_amounts(&self, session: &Session) -> Vec { let display_currency = self.get_user_preferred_currency(session); match display_currency.as_str() { "USD" => vec![ Decimal::from(10), // $10 Decimal::from(25), // $25 Decimal::from(50), // $50 Decimal::from(100), // $100 ], "EUR" => vec![ Decimal::from(10), Decimal::from(25), Decimal::from(50), Decimal::from(100), ], "GBP" => vec![ Decimal::from(10), Decimal::from(20), Decimal::from(50), Decimal::from(100), ], _ => vec![ // Default USD amounts Decimal::from(10), Decimal::from(20), Decimal::from(50), Decimal::from(100), ], } } } impl Default for CurrencyService { fn default() -> Self { Self::new() } } /// Utility functions for currency operations pub mod utils { use super::*; /// Format amount with proper decimal places for currency pub fn format_amount_with_currency( amount: Decimal, currency: &Currency, ) -> String { format!("{} {}", amount.round_dp(currency.decimal_places as u32), currency.symbol ) } /// Parse currency amount from string pub fn parse_currency_amount(amount_str: &str) -> Result { amount_str.parse::() .map_err(|e| format!("Invalid amount format: {}", e)) } /// Validate currency code format pub fn is_valid_currency_code(code: &str) -> bool { code.len() == 3 && code.chars().all(|c| c.is_ascii_uppercase()) } /// Get currency type display name pub fn get_currency_type_display(currency_type: &crate::models::currency::CurrencyType) -> &'static str { match currency_type { crate::models::currency::CurrencyType::Fiat => "Fiat Currency", crate::models::currency::CurrencyType::Cryptocurrency => "Cryptocurrency", crate::models::currency::CurrencyType::Token => "Token", crate::models::currency::CurrencyType::Points => "Loyalty Points", crate::models::currency::CurrencyType::Custom(_) => "Custom Currency", } } }