init projectmycelium

This commit is contained in:
mik-tf
2025-09-01 21:37:01 -04:00
commit b41efb0e99
319 changed files with 128160 additions and 0 deletions

536
src/services/currency.rs Normal file
View 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(&currency_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(&currency.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",
}
}
}