init projectmycelium
This commit is contained in:
536
src/services/currency.rs
Normal file
536
src/services/currency.rs
Normal file
@@ -0,0 +1,536 @@
|
||||
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<String, Decimal>,
|
||||
last_update: chrono::DateTime<chrono::Utc>,
|
||||
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: "USD".to_string(),
|
||||
};
|
||||
|
||||
// USD Credits is now the base currency - no conversion needed
|
||||
service.exchange_rates_cache.insert("USD".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<String, Decimal>,
|
||||
) -> Self {
|
||||
let mut service = Self {
|
||||
exchange_rates_cache: fallback_rates,
|
||||
last_update: Utc::now(),
|
||||
default_display_currency: "USD".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<String, Decimal>,
|
||||
) -> 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<Currency> {
|
||||
// Return standard supported currencies without mock data
|
||||
vec![
|
||||
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),
|
||||
is_base_currency: true,
|
||||
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),
|
||||
is_base_currency: false,
|
||||
decimal_places: 2,
|
||||
is_active: true,
|
||||
provider_config: None,
|
||||
last_updated: chrono::Utc::now(),
|
||||
},
|
||||
Currency {
|
||||
code: "TFC".to_string(),
|
||||
name: "ThreeFold Credits".to_string(),
|
||||
symbol: "TFC".to_string(),
|
||||
currency_type: crate::models::currency::CurrencyType::Custom("credits".to_string()),
|
||||
exchange_rate_to_base: dec!(1.0), // 1 TFC = 1 USD
|
||||
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),
|
||||
is_base_currency: false,
|
||||
decimal_places: 2,
|
||||
is_active: true,
|
||||
provider_config: None,
|
||||
last_updated: chrono::Utc::now(),
|
||||
},
|
||||
Currency {
|
||||
code: "TFT".to_string(),
|
||||
name: "ThreeFold Token".to_string(),
|
||||
symbol: "TFT".to_string(),
|
||||
currency_type: crate::models::currency::CurrencyType::Token,
|
||||
exchange_rate_to_base: dec!(0.05),
|
||||
is_base_currency: false,
|
||||
decimal_places: 3,
|
||||
is_active: true,
|
||||
provider_config: None,
|
||||
last_updated: chrono::Utc::now(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Get currency by code
|
||||
pub fn get_currency(&self, code: &str) -> Option<Currency> {
|
||||
self.get_supported_currencies()
|
||||
.into_iter()
|
||||
.find(|c| c.code == code)
|
||||
}
|
||||
|
||||
/// Get base currency
|
||||
pub fn get_base_currency(&self) -> Currency {
|
||||
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),
|
||||
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<Decimal, String> {
|
||||
if from_currency == to_currency {
|
||||
return Ok(amount);
|
||||
}
|
||||
|
||||
let base_currency_code = "USD"; // Use USD 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<Decimal, String> {
|
||||
let base_currency_code = "USD"; // Use USD 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<Price, String> {
|
||||
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<String, String> {
|
||||
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::<String>("preferred_currency") {
|
||||
return currency;
|
||||
}
|
||||
|
||||
// Then check persistent user data
|
||||
if let Ok(Some(user_email)) = session.get::<String>("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 {
|
||||
"USD".to_string() // Default to USD 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::<String>("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<String, Decimal> {
|
||||
let mut rates = HashMap::default();
|
||||
let base_currency_code = "USD".to_string(); // Use USD 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<String, serde_json::Value> {
|
||||
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("USD".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<serde_json::Value> = 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<Vec<(String, Price)>, 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<serde_json::Value> {
|
||||
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<Decimal, String> {
|
||||
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<String, String> {
|
||||
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<Decimal> {
|
||||
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<Decimal, String> {
|
||||
amount_str.parse::<Decimal>()
|
||||
.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",
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user