Files
projectmycelium/src/services/user_persistence.rs

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::ResourceProviderSettings>,
#[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::ResourceProviderRentalEarning>,
#[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::ResourceProviderSettings> {
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(())
}
}