1513 lines
54 KiB
Rust
1513 lines
54 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
use serde::{Serialize, Deserialize};
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal_macros::dec;
|
|
use crate::models::user::Transaction;
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, RwLock};
|
|
use futures_util::lock::Mutex;
|
|
use lazy_static::lazy_static;
|
|
use chrono::{Utc, DateTime};
|
|
use uuid::Uuid;
|
|
use std::time::Instant;
|
|
|
|
/// Namespace type for user persistence utilities and methods.
|
|
pub struct UserPersistence;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct UserPersistentData {
|
|
pub user_email: String,
|
|
pub wallet_balance_usd: Decimal,
|
|
pub transactions: Vec<Transaction>,
|
|
pub staked_amount_usd: Decimal,
|
|
pub pool_positions: HashMap<String, PoolPosition>,
|
|
// Profile information
|
|
pub name: Option<String>,
|
|
pub country: Option<String>,
|
|
pub timezone: Option<String>,
|
|
// Password information (hashed)
|
|
pub password_hash: Option<String>,
|
|
// Service provider data
|
|
pub services: Vec<crate::models::user::Service>,
|
|
pub service_requests: Vec<crate::models::user::ServiceRequest>,
|
|
// Customer service bookings
|
|
#[serde(default)]
|
|
pub service_bookings: Vec<crate::models::user::ServiceBooking>,
|
|
// Availability settings
|
|
pub availability: Option<AvailabilitySettings>,
|
|
// PHASE 3 FIX: SLA Management storage
|
|
pub slas: Vec<ServiceLevelAgreement>,
|
|
// App provider data
|
|
pub apps: Vec<crate::models::user::PublishedApp>,
|
|
pub app_deployments: Vec<AppDeployment>,
|
|
// Account deletion tracking
|
|
pub deleted: Option<bool>,
|
|
pub deleted_at: Option<String>,
|
|
pub deletion_reason: Option<String>,
|
|
// ResourceProvider-specific data
|
|
pub nodes: Vec<crate::models::user::FarmNode>,
|
|
pub resource_provider_earnings: Vec<crate::models::user::EarningsRecord>,
|
|
pub resource_provider_settings: Option<crate::models::user::FarmerSettings>,
|
|
#[serde(default)]
|
|
pub slice_products: Vec<crate::models::product::Product>,
|
|
// User activity tracking
|
|
pub user_activities: Vec<crate::models::user::UserActivity>,
|
|
pub user_preferences: Option<crate::models::user::UserPreferences>,
|
|
pub usage_statistics: Option<crate::models::user::UsageStatistics>,
|
|
// Order history
|
|
#[serde(default)]
|
|
pub orders: Vec<crate::models::order::Order>,
|
|
// Node rental data
|
|
#[serde(default)]
|
|
pub active_product_rentals: Vec<ProductRental>,
|
|
#[serde(default)]
|
|
pub resource_provider_rental_earnings: Vec<crate::models::user::FarmerRentalEarning>,
|
|
#[serde(default)]
|
|
pub node_rentals: Vec<crate::models::user::NodeRental>,
|
|
// Node groups for resource_provider organization
|
|
#[serde(default)]
|
|
pub node_groups: Vec<crate::models::user::NodeGroup>,
|
|
// NEW: Slice rental tracking for users
|
|
#[serde(default)]
|
|
pub slice_rentals: Vec<crate::services::slice_calculator::SliceRental>,
|
|
// Slice assignment tracking for users
|
|
#[serde(default)]
|
|
pub slice_assignments: Vec<crate::services::slice_assignment::SliceAssignment>,
|
|
// Currency preferences for OpenRouter-style enhancements
|
|
#[serde(default)]
|
|
pub display_currency: Option<String>, // "USD", "CAD", "EUR", etc.
|
|
#[serde(default)]
|
|
pub quick_topup_amounts: Option<Vec<Decimal>>, // Customizable preset amounts
|
|
// Auto top-up settings
|
|
#[serde(default)]
|
|
pub auto_topup_settings: Option<AutoTopUpSettings>,
|
|
// User-created products (applications/services for marketplace)
|
|
#[serde(default)]
|
|
pub products: Vec<crate::models::product::Product>,
|
|
// Owned products tracking
|
|
#[serde(default)]
|
|
pub owned_products: Vec<crate::models::product::Product>,
|
|
#[serde(default)]
|
|
pub owned_product_ids: Vec<String>,
|
|
// SSH key management
|
|
#[serde(default)]
|
|
pub ssh_keys: Vec<crate::models::ssh_key::SSHKey>,
|
|
// Messaging system
|
|
#[serde(default)]
|
|
pub message_threads: Option<Vec<crate::models::messaging::MessageThread>>,
|
|
#[serde(default)]
|
|
pub messages: Option<Vec<crate::models::messaging::Message>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AutoTopUpSettings {
|
|
pub enabled: bool,
|
|
pub threshold_amount_usd: Decimal,
|
|
pub topup_amount_usd: Decimal,
|
|
pub payment_method_id: String,
|
|
pub daily_limit_usd: Option<Decimal>,
|
|
pub monthly_limit_usd: Option<Decimal>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl AutoTopUpSettings {
|
|
pub fn builder() -> crate::models::builders::AutoTopUpSettingsBuilder {
|
|
crate::models::builders::AutoTopUpSettingsBuilder::new()
|
|
}
|
|
}
|
|
|
|
impl Default for UserPersistentData {
|
|
fn default() -> Self {
|
|
Self {
|
|
user_email: String::new(),
|
|
wallet_balance_usd: Decimal::ZERO,
|
|
transactions: Vec::new(),
|
|
staked_amount_usd: Decimal::ZERO,
|
|
pool_positions: HashMap::new(),
|
|
name: None,
|
|
country: None,
|
|
timezone: None,
|
|
password_hash: None,
|
|
services: Vec::new(),
|
|
service_requests: Vec::new(),
|
|
service_bookings: Vec::new(),
|
|
availability: None,
|
|
slas: Vec::new(),
|
|
apps: Vec::new(),
|
|
app_deployments: Vec::new(),
|
|
deleted: None,
|
|
deleted_at: None,
|
|
deletion_reason: None,
|
|
nodes: Vec::new(),
|
|
resource_provider_earnings: Vec::new(),
|
|
resource_provider_settings: None,
|
|
slice_products: Vec::new(),
|
|
user_activities: Vec::new(),
|
|
user_preferences: None,
|
|
usage_statistics: None,
|
|
orders: Vec::new(),
|
|
active_product_rentals: Vec::new(),
|
|
resource_provider_rental_earnings: Vec::new(),
|
|
node_rentals: Vec::new(),
|
|
node_groups: Vec::new(),
|
|
slice_rentals: Vec::new(),
|
|
slice_assignments: Vec::new(),
|
|
display_currency: Some("USD".to_string()), // Default to USD for new users
|
|
quick_topup_amounts: Some(vec![dec!(10), dec!(25), dec!(50), dec!(100)]), // USD amounts
|
|
auto_topup_settings: None, // User can configure later
|
|
products: Vec::new(), // Initialize empty products list
|
|
owned_products: Vec::new(), // Initialize empty owned products list
|
|
owned_product_ids: Vec::new(), // Initialize empty owned product IDs list
|
|
ssh_keys: Vec::new(),
|
|
message_threads: None,
|
|
messages: None, // Initialize empty SSH keys list
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Product rental record for users
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ProductRental {
|
|
pub id: String,
|
|
pub rental_id: String, // Explicit rental identifier
|
|
pub product_id: String,
|
|
pub product_name: String,
|
|
pub rental_type: String, // "slice", "full_node", "service", "app"
|
|
pub monthly_cost: Decimal,
|
|
pub start_date: String,
|
|
pub end_date: String,
|
|
pub status: String, // "Active", "Expired", "Cancelled"
|
|
pub provider_email: String,
|
|
pub customer_email: String,
|
|
pub metadata: HashMap<String, serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AvailabilitySettings {
|
|
pub available: bool,
|
|
pub weekly_hours: i32,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PoolPosition {
|
|
pub pool_id: String,
|
|
pub amount: Decimal,
|
|
pub entry_rate: Decimal,
|
|
pub timestamp: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServiceLevelAgreement {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub service_id: Option<String>, // Associated service
|
|
pub response_time_hours: i32,
|
|
pub resolution_time_hours: i32,
|
|
pub availability_percentage: f32,
|
|
pub support_hours: String, // e.g., "24/7", "Business Hours"
|
|
pub escalation_procedure: String,
|
|
pub penalties: Vec<SLAPenalty>,
|
|
pub created_at: String,
|
|
pub status: String, // Active, Inactive, Draft
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SLAPenalty {
|
|
pub breach_type: String, // Response Time, Resolution Time, Availability
|
|
pub threshold: String,
|
|
pub penalty_amount: i32,
|
|
pub penalty_type: String, // Credit, Refund, Discount
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AppDeployment {
|
|
pub id: String,
|
|
pub app_id: String,
|
|
pub app_name: String,
|
|
pub customer_name: String,
|
|
pub customer_email: String,
|
|
pub deployed_date: String,
|
|
pub created_at: String,
|
|
pub status: String, // Active, Inactive, Error, Maintenance
|
|
pub health_score: f32,
|
|
pub region: String,
|
|
pub instances: i32,
|
|
pub resource_usage: crate::models::user::ResourceUtilization,
|
|
pub monthly_revenue: i32,
|
|
pub last_updated: String,
|
|
#[serde(default)]
|
|
pub auto_healing: Option<bool>,
|
|
}
|
|
|
|
impl UserPersistence {
|
|
const DATA_DIR: &'static str = "user_data";
|
|
|
|
// Per-user async lock map to serialize modifications per user
|
|
}
|
|
|
|
// Global map of user email -> async mutex
|
|
lazy_static! {
|
|
static ref USER_LOCKS: RwLock<HashMap<String, Arc<Mutex<()>>>> = RwLock::new(HashMap::new());
|
|
}
|
|
|
|
impl UserPersistence {
|
|
/// Get or create a per-user async lock. Use as:
|
|
/// let lock = UserPersistence::get_user_lock(email);
|
|
/// let _guard = lock.lock().await; // hold for critical section
|
|
pub fn get_user_lock(user_email: &str) -> Arc<Mutex<()>> {
|
|
// Fast path: try read lock first
|
|
if let Some(lock) = USER_LOCKS.read().unwrap().get(user_email).cloned() {
|
|
return lock;
|
|
}
|
|
// Insert if missing with write lock
|
|
let mut map = USER_LOCKS.write().unwrap();
|
|
map.entry(user_email.to_string())
|
|
.or_insert_with(|| Arc::new(Mutex::new(())))
|
|
.clone()
|
|
}
|
|
|
|
fn ensure_data_dir() -> Result<(), Box<dyn std::error::Error>> {
|
|
if !Path::new(Self::DATA_DIR).exists() {
|
|
fs::create_dir_all(Self::DATA_DIR)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn get_user_file_path(user_email: &str) -> String {
|
|
// Sanitize email for filename
|
|
let sanitized = user_email.replace("@", "_at_").replace(".", "_");
|
|
format!("{}/{}.json", Self::DATA_DIR, sanitized)
|
|
}
|
|
|
|
pub fn save_user_data(data: &UserPersistentData) -> Result<(), Box<dyn std::error::Error>> {
|
|
let start_total = Instant::now();
|
|
Self::ensure_data_dir()?;
|
|
let file_path = Self::get_user_file_path(&data.user_email);
|
|
let tmp_path = format!("{}.tmp.{}", &file_path, Uuid::new_v4());
|
|
let json_data = serde_json::to_string_pretty(data)?;
|
|
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"save_user_data:start path={} email={} sizes:transactions={} services={} requests={} bookings={}",
|
|
file_path,
|
|
data.user_email,
|
|
data.transactions.len(),
|
|
data.services.len(),
|
|
data.service_requests.len(),
|
|
data.service_bookings.len()
|
|
);
|
|
|
|
// Write to a unique temp file first
|
|
{
|
|
let mut f = std::fs::OpenOptions::new()
|
|
.create_new(true)
|
|
.write(true)
|
|
.open(&tmp_path)?;
|
|
use std::io::Write as _;
|
|
f.write_all(json_data.as_bytes())?;
|
|
f.sync_all()?;
|
|
}
|
|
|
|
// Atomically rename temp file to final path
|
|
fs::rename(&tmp_path, &file_path)?;
|
|
|
|
// fsync the directory to ensure the rename is durable on disk
|
|
if let Some(parent) = Path::new(&file_path).parent() {
|
|
if let Ok(dir_file) = std::fs::File::open(parent) {
|
|
let _ = dir_file.sync_all();
|
|
}
|
|
}
|
|
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"save_user_data:success path={} email={} total_ms={}",
|
|
file_path,
|
|
data.user_email,
|
|
start_total.elapsed().as_millis()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn load_user_data(user_email: &str) -> Option<UserPersistentData> {
|
|
let start_total = Instant::now();
|
|
let file_path = Self::get_user_file_path(user_email);
|
|
|
|
if Path::new(&file_path).exists() {
|
|
match fs::read_to_string(&file_path) {
|
|
Ok(json_data) => {
|
|
match serde_json::from_str::<UserPersistentData>(&json_data) {
|
|
Ok(data) => {
|
|
log::debug!(
|
|
target: "user_persistence",
|
|
"load_user_data:success path={} email={} total_ms={}",
|
|
file_path,
|
|
user_email,
|
|
start_total.elapsed().as_millis()
|
|
);
|
|
Some(data)
|
|
},
|
|
Err(e) => {
|
|
// CRITICAL: Don't fall back to mock data for existing files!
|
|
// This would cause data loss. The #[serde(default)] should handle missing fields.
|
|
log::error!(
|
|
target: "user_persistence",
|
|
"load_user_data:parse_error path={} email={} err={}",
|
|
file_path,
|
|
user_email,
|
|
e
|
|
);
|
|
None
|
|
}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
log::error!(
|
|
target: "user_persistence",
|
|
"load_user_data:read_error path={} email={} err={}",
|
|
file_path,
|
|
user_email,
|
|
e
|
|
);
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
log::debug!(
|
|
target: "user_persistence",
|
|
"load_user_data:not_found path={} email={} total_ms={}",
|
|
file_path,
|
|
user_email,
|
|
start_total.elapsed().as_millis()
|
|
);
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Async wrapper that acquires the per-user lock before loading, with structured logging
|
|
pub async fn load_user_data_locked(user_email: &str, req_id: Option<&str>) -> Option<UserPersistentData> {
|
|
let lock = Self::get_user_lock(user_email);
|
|
let wait_start = Instant::now();
|
|
log::debug!(
|
|
target: "user_persistence",
|
|
"load_user_data_locked:waiting_for_lock email={}{}",
|
|
user_email,
|
|
req_id.map(|r| format!(" req_id={}", r)).unwrap_or_default()
|
|
);
|
|
let _g = lock.lock().await;
|
|
let wait_ms = wait_start.elapsed().as_millis();
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"load_user_data_locked:lock_acquired email={} wait_ms={}{}",
|
|
user_email,
|
|
wait_ms,
|
|
req_id.map(|r| format!(" req_id={}", r)).unwrap_or_default()
|
|
);
|
|
Self::load_user_data(user_email)
|
|
}
|
|
|
|
/// Async wrapper that acquires the per-user lock before saving, with structured logging
|
|
pub async fn save_user_data_locked(data: &UserPersistentData, req_id: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
|
let lock = Self::get_user_lock(&data.user_email);
|
|
let wait_start = Instant::now();
|
|
log::debug!(
|
|
target: "user_persistence",
|
|
"save_user_data_locked:waiting_for_lock email={}{}",
|
|
data.user_email,
|
|
req_id.map(|r| format!(" req_id={}", r)).unwrap_or_default()
|
|
);
|
|
let _g = lock.lock().await;
|
|
let wait_ms = wait_start.elapsed().as_millis();
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"save_user_data_locked:lock_acquired email={} wait_ms={}{}",
|
|
data.user_email,
|
|
wait_ms,
|
|
req_id.map(|r| format!(" req_id={}", r)).unwrap_or_default()
|
|
);
|
|
Self::save_user_data(data)
|
|
}
|
|
|
|
fn initialize_new_user_with_defaults(user_email: &str) -> Option<UserPersistentData> {
|
|
// Use the existing create_default_user_data method for consistency
|
|
let initial_data = Self::create_default_user_data(user_email);
|
|
|
|
// Save the initial data
|
|
if let Err(e) = Self::save_user_data(&initial_data) {
|
|
return None;
|
|
}
|
|
Some(initial_data)
|
|
}
|
|
|
|
// Keep the old function for backward compatibility where mock data is still needed
|
|
// (e.g., for incomplete features like mycelium gateways)
|
|
fn initialize_user_with_mock_balance(user_email: &str) -> Option<UserPersistentData> {
|
|
// Initialize with clean persistent data - no mock dependencies
|
|
let initial_data = crate::models::builders::SessionDataBuilder::new_user(user_email);
|
|
|
|
// Save the initial data
|
|
if let Err(e) = Self::save_user_data(&initial_data) {
|
|
return None;
|
|
}
|
|
Some(initial_data)
|
|
}
|
|
|
|
pub fn update_user_profile(
|
|
user_email: &str,
|
|
name: Option<String>,
|
|
country: Option<String>,
|
|
timezone: Option<String>
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Load existing data or create new
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Update profile fields
|
|
if let Some(n) = name {
|
|
data.name = Some(n);
|
|
}
|
|
if let Some(c) = country {
|
|
data.country = Some(c);
|
|
}
|
|
if let Some(t) = timezone {
|
|
data.timezone = Some(t);
|
|
}
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_user_password(
|
|
user_email: &str,
|
|
password_hash: String
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Load existing data or create new
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Update password hash
|
|
data.password_hash = Some(password_hash);
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn update_notification_settings(
|
|
user_email: &str,
|
|
notification_settings: crate::models::user::NotificationSettings
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Load existing data or create new
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Initialize user_preferences if it doesn't exist
|
|
if data.user_preferences.is_none() {
|
|
data.user_preferences = Some(crate::models::user::UserPreferences::default());
|
|
}
|
|
|
|
// Update notification settings within user_preferences
|
|
if let Some(ref mut prefs) = data.user_preferences {
|
|
prefs.notification_settings = Some(notification_settings);
|
|
}
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn delete_user_data(user_email: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
let file_path = Self::get_user_file_path(user_email);
|
|
if Path::new(&file_path).exists() {
|
|
fs::remove_file(file_path)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Soft delete user account - marks as deleted instead of removing data
|
|
pub fn soft_delete_user_account(
|
|
user_email: &str,
|
|
deletion_reason: Option<String>
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let data = match Self::load_user_data(user_email) {
|
|
Some(mut data) => {
|
|
// Mark account as deleted
|
|
data.deleted = Some(true);
|
|
data.deleted_at = Some(Utc::now().to_rfc3339());
|
|
data.deletion_reason = deletion_reason;
|
|
data
|
|
},
|
|
None => {
|
|
// Create minimal data for deletion record using centralized builder
|
|
let mut data = crate::models::builders::SessionDataBuilder::new_user(user_email);
|
|
data.deleted = Some(true);
|
|
data.deleted_at = Some(Utc::now().to_rfc3339());
|
|
data.deletion_reason = deletion_reason;
|
|
data
|
|
}
|
|
};
|
|
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if user account is deleted
|
|
pub fn is_user_deleted(user_email: &str) -> bool {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.deleted.unwrap_or(false)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Restore deleted user account (for recovery purposes)
|
|
pub fn restore_user_account(user_email: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no account to restore
|
|
};
|
|
|
|
if data.deleted.unwrap_or(false) {
|
|
data.deleted = None;
|
|
data.deleted_at = None;
|
|
data.deletion_reason = None;
|
|
Self::save_user_data(&data)?;
|
|
Ok(true)
|
|
} else {
|
|
Ok(false) // Account was not deleted
|
|
}
|
|
}
|
|
|
|
/// Add a service to user's persistent data
|
|
pub fn add_user_service(
|
|
user_email: &str,
|
|
service: crate::models::user::Service
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Load existing data or create new using centralized builder
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Add service if not already present
|
|
if !data.services.iter().any(|s| s.id == service.id) {
|
|
data.services.push(service.clone());
|
|
} else {
|
|
}
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get all services for a user
|
|
pub fn get_user_services(user_email: &str) -> Vec<crate::models::user::Service> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.services
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Remove a service from user's persistent data
|
|
pub fn remove_user_service(
|
|
user_email: &str,
|
|
service_id: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no service to remove
|
|
};
|
|
|
|
let initial_len = data.services.len();
|
|
data.services.retain(|s| s.id != service_id);
|
|
let removed = data.services.len() < initial_len;
|
|
|
|
if removed {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
/// Get all services from all users for marketplace aggregation (excludes deleted users)
|
|
pub fn get_all_users_services() -> Vec<crate::models::user::Service> {
|
|
let mut all_services = Vec::default();
|
|
|
|
// Get all user data files
|
|
if let Ok(entries) = fs::read_dir(Self::DATA_DIR) {
|
|
for entry in entries.flatten() {
|
|
if let Some(file_name) = entry.file_name().to_str() {
|
|
// Only process per-user files, e.g. user1_at_example_com.json
|
|
let is_user_file = file_name.ends_with(".json")
|
|
&& file_name.contains("_at_")
|
|
&& !file_name.contains("_cart");
|
|
if is_user_file {
|
|
// Extract email from filename
|
|
let email = file_name
|
|
.trim_end_matches(".json")
|
|
.replace("_at_", "@")
|
|
.replace("_", ".");
|
|
|
|
// Skip deleted users
|
|
if Self::is_user_deleted(&email) {
|
|
continue;
|
|
}
|
|
|
|
// Load user services
|
|
let user_services = Self::get_user_services(&email);
|
|
let service_count = user_services.len();
|
|
all_services.extend(user_services);
|
|
|
|
if service_count > 0 {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
all_services
|
|
}
|
|
/// Get all apps from all users for marketplace aggregation (excludes deleted users)
|
|
pub fn get_all_users_apps() -> Vec<crate::models::user::PublishedApp> {
|
|
let mut all_apps = Vec::default();
|
|
|
|
// Get all user data files
|
|
if let Ok(entries) = fs::read_dir(Self::DATA_DIR) {
|
|
for entry in entries.flatten() {
|
|
if let Some(file_name) = entry.file_name().to_str() {
|
|
// Only process per-user files, e.g. user1_at_example_com.json
|
|
let is_user_file = file_name.ends_with(".json")
|
|
&& file_name.contains("_at_")
|
|
&& !file_name.contains("_cart");
|
|
if is_user_file {
|
|
// Extract email from filename
|
|
let email = file_name
|
|
.trim_end_matches(".json")
|
|
.replace("_at_", "@")
|
|
.replace("_", ".");
|
|
|
|
// Skip deleted users
|
|
if Self::is_user_deleted(&email) {
|
|
continue;
|
|
}
|
|
|
|
// Load user apps
|
|
let user_apps = Self::get_user_apps(&email);
|
|
let app_count = user_apps.len();
|
|
all_apps.extend(user_apps);
|
|
|
|
if app_count > 0 {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
all_apps
|
|
}
|
|
|
|
/// Add a product to user's persistent data
|
|
pub fn add_user_product(
|
|
user_email: &str,
|
|
product: crate::models::product::Product
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Load existing data or create new using centralized builder
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Add product if not already present
|
|
if !data.products.iter().any(|p| p.id == product.id) {
|
|
data.products.push(product.clone());
|
|
}
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get all products for a user
|
|
pub fn get_user_products(user_email: &str) -> Vec<crate::models::product::Product> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.products
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Remove a product from user's persistent data
|
|
pub fn remove_user_product(
|
|
user_email: &str,
|
|
product_id: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no product to remove
|
|
};
|
|
|
|
let initial_len = data.products.len();
|
|
data.products.retain(|p| p.id != product_id);
|
|
let removed = data.products.len() < initial_len;
|
|
|
|
if removed {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
/// Get all products from all users for marketplace aggregation (excludes deleted users)
|
|
pub fn get_all_users_products() -> Vec<crate::models::product::Product> {
|
|
let mut all_products = Vec::default();
|
|
|
|
println!("🔍 USER PERSISTENCE: Looking for user data files in {}", Self::DATA_DIR);
|
|
// Get all user data files
|
|
if let Ok(entries) = fs::read_dir(Self::DATA_DIR) {
|
|
let mut file_count = 0;
|
|
for entry in entries.flatten() {
|
|
if let Some(file_name) = entry.file_name().to_str() {
|
|
// Only consider actual per-user files, e.g. user1_at_example_com.json
|
|
let is_user_file = file_name.ends_with(".json")
|
|
&& file_name.contains("_at_")
|
|
&& !file_name.contains("_cart");
|
|
if is_user_file {
|
|
file_count += 1;
|
|
// Extract email from filename
|
|
let email = file_name
|
|
.trim_end_matches(".json")
|
|
.replace("_at_", "@")
|
|
.replace("_", ".");
|
|
|
|
println!(
|
|
"🔍 USER PERSISTENCE: Processing user file: {} -> email: {}",
|
|
file_name, email
|
|
);
|
|
|
|
// Skip deleted users
|
|
if Self::is_user_deleted(&email) {
|
|
println!("🔍 USER PERSISTENCE: Skipping deleted user: {}", email);
|
|
continue;
|
|
}
|
|
|
|
// Load user products
|
|
let user_products = Self::get_user_products(&email);
|
|
println!(
|
|
"🔍 USER PERSISTENCE: User {} has {} products",
|
|
email,
|
|
user_products.len()
|
|
);
|
|
all_products.extend(user_products);
|
|
}
|
|
}
|
|
}
|
|
println!("🔍 USER PERSISTENCE: Processed {} JSON files, found {} total products", file_count, all_products.len());
|
|
} else {
|
|
println!("🔍 USER PERSISTENCE: Could not read directory {}", Self::DATA_DIR);
|
|
}
|
|
all_products
|
|
}
|
|
|
|
/// Update user availability settings
|
|
pub fn update_user_availability(
|
|
user_email: &str,
|
|
availability: AvailabilitySettings
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Load existing data or create new using centralized builder
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Update availability settings
|
|
data.availability = Some(availability);
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get user availability settings
|
|
pub fn get_user_availability(user_email: &str) -> Option<AvailabilitySettings> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.availability
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Add a service request to user's persistent data
|
|
pub fn add_user_service_request(
|
|
user_email: &str,
|
|
service_request: crate::models::user::ServiceRequest
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Add service request if not already present
|
|
if !data.service_requests.iter().any(|r| r.id == service_request.id) {
|
|
data.service_requests.push(service_request.clone());
|
|
} else {
|
|
}
|
|
|
|
// Save updated data
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"add_user_service_request:before_save email={} total_requests={} added_request_id={}",
|
|
user_email,
|
|
data.service_requests.len(),
|
|
service_request.id
|
|
);
|
|
Self::save_user_data(&data)?;
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"add_user_service_request:after_save email={} total_requests={}",
|
|
user_email,
|
|
data.service_requests.len()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get all service requests for a user
|
|
pub fn get_user_service_requests(user_email: &str) -> Vec<crate::models::user::ServiceRequest> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.service_requests
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Update service request status
|
|
pub fn update_service_request_status(
|
|
user_email: &str,
|
|
request_id: &str,
|
|
new_status: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no request to update
|
|
};
|
|
|
|
let mut updated = false;
|
|
for request in &mut data.service_requests {
|
|
if request.id == request_id {
|
|
request.status = new_status.to_string();
|
|
// If marking as completed, set completed_date; otherwise leave as-is
|
|
if new_status == "Completed" {
|
|
request.completed_date = Some(chrono::Utc::now().format("%Y-%m-%d").to_string());
|
|
}
|
|
updated = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(updated)
|
|
}
|
|
|
|
/// Update service request progress and related data
|
|
pub fn update_service_request_progress(
|
|
user_email: &str,
|
|
request_id: &str,
|
|
progress: i32,
|
|
priority: Option<&str>,
|
|
hours_worked: Option<f64>,
|
|
notes: Option<&str>,
|
|
new_status: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no request to update
|
|
};
|
|
|
|
let mut updated = false;
|
|
for request in &mut data.service_requests {
|
|
if request.id == request_id {
|
|
// Update status
|
|
request.status = new_status.to_string();
|
|
|
|
// Update progress field
|
|
request.progress = Some(progress as f32);
|
|
|
|
// Update priority if provided
|
|
if let Some(priority) = priority {
|
|
request.priority = priority.to_string();
|
|
}
|
|
|
|
// Persist hours worked and notes when provided
|
|
if let Some(hours) = hours_worked {
|
|
request.hours_worked = Some(hours as i32);
|
|
}
|
|
if let Some(notes) = notes {
|
|
request.notes = Some(notes.to_string());
|
|
}
|
|
|
|
// If completed, set completed_date
|
|
if new_status == "Completed" || progress >= 100 {
|
|
request.completed_date = Some(chrono::Utc::now().format("%Y-%m-%d").to_string());
|
|
}
|
|
|
|
updated = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(updated)
|
|
}
|
|
|
|
/// Update a customer's service booking fields by booking id
|
|
/// Returns Ok(true) if updated and saved, Ok(false) if booking not found or user has no data
|
|
pub fn update_user_service_booking_fields(
|
|
user_email: &str,
|
|
booking_id: &str,
|
|
status: Option<&str>,
|
|
progress: Option<i32>,
|
|
priority: Option<&str>,
|
|
completed_date: Option<&str>,
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false),
|
|
};
|
|
|
|
let mut updated = false;
|
|
for booking in &mut data.service_bookings {
|
|
if booking.id == booking_id {
|
|
if let Some(s) = status { booking.status = s.to_string(); }
|
|
if let Some(p) = progress { booking.progress = Some(p as f32); }
|
|
if let Some(pr) = priority { booking.priority = pr.to_string(); }
|
|
if let Some(cd) = completed_date { booking.completed_date = Some(cd.to_string()); }
|
|
|
|
updated = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"update_user_service_booking_fields:before_save email={} booking_id={} status={:?} progress={:?} priority={:?}",
|
|
user_email, booking_id, status, progress, priority
|
|
);
|
|
Self::save_user_data(&data)?;
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"update_user_service_booking_fields:after_save email={} total_bookings={}",
|
|
user_email, data.service_bookings.len()
|
|
);
|
|
}
|
|
|
|
Ok(updated)
|
|
}
|
|
|
|
/// Remove a service request from user's persistent data
|
|
pub fn remove_user_service_request(
|
|
user_email: &str,
|
|
request_id: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no request to remove
|
|
};
|
|
|
|
let initial_len = data.service_requests.len();
|
|
data.service_requests.retain(|r| r.id != request_id);
|
|
let removed = data.service_requests.len() < initial_len;
|
|
|
|
if removed {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SERVICE BOOKING METHODS (Customer side of service purchases)
|
|
// =============================================================================
|
|
|
|
/// Add a service booking to customer's data
|
|
pub fn add_user_service_booking(
|
|
user_email: &str,
|
|
service_booking: crate::models::user::ServiceBooking
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = Self::load_user_data(user_email)
|
|
.unwrap_or_else(|| Self::create_default_user_data(user_email));
|
|
|
|
// Add service booking if not already present
|
|
if !data.service_bookings.iter().any(|b| b.id == service_booking.id) {
|
|
data.service_bookings.push(service_booking.clone());
|
|
} else {
|
|
}
|
|
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"add_user_service_booking:before_save email={} total_bookings={} added_booking_id={}",
|
|
user_email,
|
|
data.service_bookings.len(),
|
|
service_booking.id
|
|
);
|
|
Self::save_user_data(&data)?;
|
|
log::info!(
|
|
target: "user_persistence",
|
|
"add_user_service_booking:after_save email={} total_bookings={}",
|
|
user_email,
|
|
data.service_bookings.len()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get customer's service bookings
|
|
pub fn get_user_service_bookings(user_email: &str) -> Vec<crate::models::user::ServiceBooking> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.service_bookings
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Convert ServiceRequest to ServiceBooking for customer
|
|
pub fn create_service_booking_from_request(
|
|
request: &crate::models::user::ServiceRequest,
|
|
customer_email: &str,
|
|
provider_email: &str
|
|
) -> crate::models::user::ServiceBooking {
|
|
crate::models::user::ServiceBooking::builder()
|
|
.id(&request.id)
|
|
.service_id(&format!("svc_{}", &request.id[4..])) // Extract service ID from request ID
|
|
.service_name(&request.service_name)
|
|
.provider_email(provider_email)
|
|
.customer_email(customer_email)
|
|
.budget(request.budget)
|
|
.estimated_hours(request.estimated_hours.unwrap_or(0))
|
|
.status(&request.status)
|
|
.requested_date(&request.requested_date)
|
|
.priority(&request.priority)
|
|
.description(request.description.clone())
|
|
.booking_date(&chrono::Utc::now().format("%Y-%m-%d").to_string())
|
|
.build()
|
|
.unwrap()
|
|
}
|
|
|
|
/// PHASE 3 FIX: Add a SLA to user's persistent data
|
|
pub fn add_user_sla(
|
|
user_email: &str,
|
|
sla: ServiceLevelAgreement
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Add SLA if not already present
|
|
if !data.slas.iter().any(|s| s.id == sla.id) {
|
|
data.slas.push(sla.clone());
|
|
} else {
|
|
}
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// PHASE 3 FIX: Get all SLAs for a user
|
|
pub fn get_user_slas(user_email: &str) -> Vec<ServiceLevelAgreement> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.slas
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// PHASE 3 FIX: Update a SLA
|
|
pub fn update_user_sla(
|
|
user_email: &str,
|
|
sla: ServiceLevelAgreement
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no SLA to update
|
|
};
|
|
|
|
let mut updated = false;
|
|
for existing_sla in &mut data.slas {
|
|
if existing_sla.id == sla.id {
|
|
*existing_sla = sla.clone();
|
|
updated = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(updated)
|
|
}
|
|
|
|
/// PHASE 3 FIX: Remove a SLA from user's persistent data
|
|
pub fn remove_user_sla(
|
|
user_email: &str,
|
|
sla_id: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no SLA to remove
|
|
};
|
|
|
|
let initial_len = data.slas.len();
|
|
data.slas.retain(|s| s.id != sla_id);
|
|
let removed = data.slas.len() < initial_len;
|
|
|
|
if removed {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
/// PHASE 3 FIX: Get SLA by ID
|
|
pub fn get_user_sla_by_id(user_email: &str, sla_id: &str) -> Option<ServiceLevelAgreement> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.slas.into_iter().find(|s| s.id == sla_id)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// APP PROVIDER MANAGEMENT FUNCTIONS
|
|
// ========================================
|
|
|
|
/// Get all apps for a user
|
|
pub fn get_user_apps(user_email: &str) -> Vec<crate::models::user::PublishedApp> {
|
|
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
for app in &data.apps {
|
|
}
|
|
data.apps
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Add a new app to user's persistent data
|
|
pub fn add_user_app(
|
|
user_email: &str,
|
|
app: crate::models::user::PublishedApp
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
data.apps.push(app.clone());
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Update an existing app in user's persistent data
|
|
pub fn update_user_app(
|
|
user_email: &str,
|
|
updated_app: crate::models::user::PublishedApp
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no app to update
|
|
};
|
|
|
|
let mut updated = false;
|
|
for app in &mut data.apps {
|
|
if app.id == updated_app.id {
|
|
*app = updated_app.clone();
|
|
updated = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(updated)
|
|
}
|
|
|
|
/// Remove an app from user's persistent data
|
|
pub fn remove_user_app(
|
|
user_email: &str,
|
|
app_id: &str
|
|
) -> Result<bool, Box<dyn std::error::Error>> {
|
|
let mut data = match Self::load_user_data(user_email) {
|
|
Some(data) => data,
|
|
None => return Ok(false), // No data means no app to remove
|
|
};
|
|
|
|
let initial_len = data.apps.len();
|
|
data.apps.retain(|app| app.id != app_id);
|
|
let removed = data.apps.len() < initial_len;
|
|
|
|
if removed {
|
|
Self::save_user_data(&data)?;
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
/// Get app deployments for a user
|
|
pub fn get_user_app_deployments(user_email: &str) -> Vec<AppDeployment> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.app_deployments
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Add a new app deployment
|
|
pub fn add_user_app_deployment(
|
|
user_email: &str,
|
|
deployment: AppDeployment
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
data.app_deployments.push(deployment.clone());
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Load apps for user directory (for marketplace integration)
|
|
pub fn load_apps_for_user(user_email: &str) -> Vec<crate::models::user::PublishedApp> {
|
|
|
|
// Check if user has an apps.json file
|
|
let user_dir = format!("./user_data/{}", user_email);
|
|
let apps_file = format!("{}/apps.json", user_dir);
|
|
|
|
if Path::new(&apps_file).exists() {
|
|
match fs::read_to_string(&apps_file) {
|
|
Ok(content) => {
|
|
match serde_json::from_str::<Vec<crate::models::user::PublishedApp>>(&content) {
|
|
Ok(apps) => {
|
|
for app in &apps {
|
|
}
|
|
apps
|
|
},
|
|
Err(e) => {
|
|
Vec::default()
|
|
}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
Vec::default()
|
|
}
|
|
}
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Get all nodes for a user
|
|
pub fn get_user_nodes(user_email: &str) -> Vec<crate::models::user::FarmNode> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.nodes
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Get resource_provider earnings for a user
|
|
pub fn get_resource_provider_earnings(user_email: &str) -> Vec<crate::models::user::EarningsRecord> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.resource_provider_earnings
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Get resource_provider settings for a user
|
|
pub fn get_resource_provider_settings(user_email: &str) -> Option<crate::models::user::FarmerSettings> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.resource_provider_settings
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Get slice products for a user
|
|
pub fn get_slice_products(user_email: &str) -> Vec<crate::models::product::Product> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.slice_products
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Get user slice products (alias for consistency)
|
|
pub fn get_user_slice_products(user_email: &str) -> Vec<crate::models::product::Product> {
|
|
Self::get_slice_products(user_email)
|
|
}
|
|
|
|
/// Save multiple slice products
|
|
pub fn save_user_slice_products(user_email: &str, products: &[crate::models::product::Product]) -> Result<(), String> {
|
|
let mut data = Self::load_user_data(user_email).unwrap_or_else(|| {
|
|
Self::create_default_user_data(user_email)
|
|
});
|
|
data.slice_products = products.to_vec();
|
|
Self::save_user_data(&data).map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Create default user data with industry-standard factory method
|
|
pub fn create_default_user_data(user_email: &str) -> UserPersistentData {
|
|
UserPersistentData {
|
|
user_email: user_email.to_string(),
|
|
wallet_balance_usd: dec!(0),
|
|
display_currency: Some("USD".to_string()),
|
|
quick_topup_amounts: Some(vec![dec!(10), dec!(25), dec!(50), dec!(100)]),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Create user data with specific balance
|
|
pub fn create_user_with_balance(user_email: &str, balance: Decimal) -> UserPersistentData {
|
|
UserPersistentData {
|
|
user_email: user_email.to_string(),
|
|
wallet_balance_usd: balance,
|
|
display_currency: Some("USD".to_string()),
|
|
quick_topup_amounts: Some(vec![dec!(10), dec!(25), dec!(50), dec!(100)]),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Load user data or create default if not found
|
|
pub fn load_or_create_user(user_email: &str) -> UserPersistentData {
|
|
Self::load_user_data(user_email)
|
|
.unwrap_or_else(|| Self::create_default_user_data(user_email))
|
|
}
|
|
|
|
/// Save slice product
|
|
pub fn save_slice_product(user_email: &str, product: crate::models::product::Product) -> Result<(), String> {
|
|
let mut data = Self::load_user_data(user_email).unwrap_or_else(|| {
|
|
Self::create_default_user_data(user_email)
|
|
});
|
|
|
|
// Check if product already exists and update, otherwise add new
|
|
if let Some(existing_index) = data.slice_products.iter().position(|p| p.id == product.id) {
|
|
data.slice_products[existing_index] = product;
|
|
} else {
|
|
data.slice_products.push(product);
|
|
}
|
|
|
|
Self::save_user_data(&data).map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Delete slice product
|
|
pub fn delete_slice_product(user_email: &str, product_id: &str) -> Result<(), String> {
|
|
let mut data = Self::load_user_data(user_email).unwrap_or_else(|| {
|
|
Self::create_default_user_data(user_email)
|
|
});
|
|
|
|
data.slice_products.retain(|p| p.id != product_id);
|
|
|
|
Self::save_user_data(&data).map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// Get user activities
|
|
pub fn get_user_activities(user_email: &str, limit: Option<usize>) -> Vec<crate::models::user::UserActivity> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
let mut activities = data.user_activities;
|
|
if let Some(limit) = limit {
|
|
activities.truncate(limit);
|
|
}
|
|
activities
|
|
} else {
|
|
Vec::default()
|
|
}
|
|
}
|
|
|
|
/// Get user statistics
|
|
pub fn get_user_statistics(user_email: &str) -> Option<crate::models::user::UsageStatistics> {
|
|
if let Some(data) = Self::load_user_data(user_email) {
|
|
data.usage_statistics
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Calculate user statistics - simplified version
|
|
pub fn calculate_user_statistics(_user_email: &str) -> Result<crate::models::user::UsageStatistics, Box<dyn std::error::Error>> {
|
|
// Return a basic statistics object using existing fields
|
|
Ok(crate::models::user::UsageStatistics {
|
|
cpu_usage: 0.0,
|
|
memory_usage: 0.0,
|
|
storage_usage: 0.0,
|
|
network_usage: 0.0,
|
|
active_services: 0,
|
|
total_spent: rust_decimal_macros::dec!(0),
|
|
favorite_categories: Vec::new(),
|
|
usage_trends: Vec::new(),
|
|
login_frequency: 1.0,
|
|
total_deployments: 0,
|
|
preferred_regions: Vec::new(),
|
|
account_age_days: 30,
|
|
last_activity: chrono::Utc::now(),
|
|
})
|
|
}
|
|
|
|
/// Add a node to user's persistent data
|
|
pub fn add_user_node(
|
|
user_email: &str,
|
|
node: crate::models::user::FarmNode
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Check for duplicates by both node ID and grid node ID
|
|
let node_exists = data.nodes.iter().any(|n| {
|
|
n.id == node.id ||
|
|
(n.grid_node_id.is_some() && node.grid_node_id.is_some() && n.grid_node_id == node.grid_node_id)
|
|
});
|
|
|
|
if !node_exists {
|
|
data.nodes.push(node.clone());
|
|
|
|
// Save updated data immediately
|
|
Self::save_user_data(&data)?;
|
|
} else {
|
|
return Err("Node already exists".into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a node from user's persistent data
|
|
pub fn remove_user_node(
|
|
user_email: &str,
|
|
node_id: &str
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = Self::load_user_data(user_email)
|
|
.ok_or("User data not found")?;
|
|
|
|
// Find and remove the node
|
|
let initial_count = data.nodes.len();
|
|
data.nodes.retain(|node| node.id != node_id);
|
|
|
|
if data.nodes.len() < initial_count {
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
} else {
|
|
Err(format!("Node '{}' not found for user: {}", node_id, user_email).into())
|
|
}
|
|
}
|
|
|
|
/// Add a user activity
|
|
pub fn add_user_activity(
|
|
user_email: &str,
|
|
activity: crate::models::user::UserActivity
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
|
|
|
// Add activity
|
|
data.user_activities.push(activity);
|
|
|
|
// Keep only the last 100 activities to prevent unlimited growth
|
|
if data.user_activities.len() > 100 {
|
|
data.user_activities.drain(0..data.user_activities.len() - 100);
|
|
}
|
|
|
|
// Save updated data
|
|
Self::save_user_data(&data)?;
|
|
Ok(())
|
|
}
|
|
} |