From 6569e819aea0c3b5c0d96455f585339d5407c19b Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Thu, 21 Aug 2025 14:07:14 +0200 Subject: [PATCH] marketplace models wip --- .../src/models/tfmarketplace/activity.rs | 115 + heromodels/src/models/tfmarketplace/app.rs | 361 ++ .../src/models/tfmarketplace/builders.rs | 351 ++ heromodels/src/models/tfmarketplace/cart.rs | 105 + .../src/models/tfmarketplace/currency.rs | 90 + heromodels/src/models/tfmarketplace/farmer.rs | 30 + heromodels/src/models/tfmarketplace/mod.rs | 17 + heromodels/src/models/tfmarketplace/node.rs | 1660 ++++++++ heromodels/src/models/tfmarketplace/notes.md | 8 + heromodels/src/models/tfmarketplace/order.rs | 402 ++ .../src/models/tfmarketplace/payment.rs | 77 + heromodels/src/models/tfmarketplace/pool.rs | 105 + .../src/models/tfmarketplace/product.rs | 660 ++++ .../src/models/tfmarketplace/service.rs | 297 ++ heromodels/src/models/tfmarketplace/slice.rs | 200 + heromodels/src/models/tfmarketplace/user.rs | 3509 +++++++++++++++++ 16 files changed, 7987 insertions(+) create mode 100644 heromodels/src/models/tfmarketplace/activity.rs create mode 100644 heromodels/src/models/tfmarketplace/app.rs create mode 100644 heromodels/src/models/tfmarketplace/builders.rs create mode 100644 heromodels/src/models/tfmarketplace/cart.rs create mode 100644 heromodels/src/models/tfmarketplace/currency.rs create mode 100644 heromodels/src/models/tfmarketplace/farmer.rs create mode 100644 heromodels/src/models/tfmarketplace/mod.rs create mode 100644 heromodels/src/models/tfmarketplace/node.rs create mode 100644 heromodels/src/models/tfmarketplace/notes.md create mode 100644 heromodels/src/models/tfmarketplace/order.rs create mode 100644 heromodels/src/models/tfmarketplace/payment.rs create mode 100644 heromodels/src/models/tfmarketplace/pool.rs create mode 100644 heromodels/src/models/tfmarketplace/product.rs create mode 100644 heromodels/src/models/tfmarketplace/service.rs create mode 100644 heromodels/src/models/tfmarketplace/slice.rs create mode 100644 heromodels/src/models/tfmarketplace/user.rs diff --git a/heromodels/src/models/tfmarketplace/activity.rs b/heromodels/src/models/tfmarketplace/activity.rs new file mode 100644 index 0000000..2921df5 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/activity.rs @@ -0,0 +1,115 @@ +use heromodels_core::BaseModelData; +use crate::models::tfmarketplace::user::ResourceUtilization; +#[derive(Default)] +pub struct UserActivityBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + activity_type: Option, + description: Option, + timestamp: Option>, + metadata: Option>, + category: Option, + importance: Option, +} + +impl UserActivityBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self) -> Self{ + self.base_data.id = Some(id.into()); + self + } + + pub fn activity_type(mut self, activity_type: crate::models::user::ActivityType) -> Self { + self.activity_type = Some(activity_type); + self + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn timestamp(mut self, timestamp: chrono::DateTime) -> Self { + self.timestamp = Some(timestamp); + self + } + + pub fn metadata(mut self, metadata: std::collections::HashMap) -> Self { + self.metadata = Some(metadata); + self + } + + pub fn category(mut self, category: impl Into) -> Self { + self.category = Some(category.into()); + self + } + + pub fn importance(mut self, importance: crate::models::user::ActivityImportance) -> Self { + self.importance = Some(importance); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::UserActivity { + base_data: BaseModelData::new(), + // id: self.base_data.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()) - moved to base_data, + activity_type: self.activity_type.ok_or("activity_type is required")?, + description: self.description.unwrap_or_else(|| "No description".to_string()), + timestamp: self.timestamp.unwrap_or_else(|| chrono::Utc::now()), + metadata: self.metadata.unwrap_or_default(), + category: self.category.unwrap_or_else(|| "General".to_string()), + importance: self.importance.unwrap_or(crate::models::user::ActivityImportance::Medium), + }) + } +} + + + +/// User Activity Tracking +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserActivity { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub activity_type: ActivityType, + pub description: String, + #[serde(deserialize_with = "deserialize_datetime")] + pub timestamp: DateTime, + pub metadata: std::collections::HashMap, + pub category: String, + pub importance: ActivityImportance, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ActivityType { + Login, + Purchase, + Deployment, + ServiceCreated, + AppPublished, + NodeAdded, + NodeUpdated, + WalletTransaction, + ProfileUpdate, + SettingsChange, + MarketplaceView, + SliceCreated, + SliceAllocated, + SliceReleased, + SliceRentalStarted, + SliceRentalStopped, + SliceRentalRestarted, + SliceRentalCancelled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ActivityImportance { + Low, + Medium, + High, + Critical, +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/app.rs b/heromodels/src/models/tfmarketplace/app.rs new file mode 100644 index 0000000..be8c401 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/app.rs @@ -0,0 +1,361 @@ +use heromodels_core::BaseModelData; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +/// Unified App struct that can represent published apps, deployments, and deployment stats +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct App { + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + + // Core app information + pub name: String, + pub category: Option, + pub version: Option, + pub status: String, + + // Deployment information + pub customer_name: Option, + pub customer_email: Option, + pub deployed_date: Option, + pub health_score: Option, + pub region: Option, + pub instances: Option, + pub resource_usage: Option, + + // Business metrics + pub deployments: Option, + pub rating: Option, + pub monthly_revenue_usd: Option, + pub cost_per_month: Option, + + // Metadata + pub last_updated: Option, + pub auto_healing: Option, + pub provider: Option, + pub deployed_at: Option>, +} + +impl App { + /// Convenience method to get the app ID + pub fn id(&self) -> &u32 { + &self.base_data.id + } + + + /// Get category with default + pub fn category_or_default(&self) -> String { + self.category.clone().unwrap_or_else(|| "Application".to_string()) + } + + /// Get version with default + pub fn version_or_default(&self) -> String { + self.version.clone().unwrap_or_else(|| "1.0.0".to_string()) + } + + /// Get deployments count with default + pub fn deployments_or_default(&self) -> i32 { + self.deployments.unwrap_or(0) + } + + /// Get rating with default + pub fn rating_or_default(&self) -> f32 { + self.rating.unwrap_or(4.0) + } + + /// Get monthly revenue with default + pub fn monthly_revenue_usd_or_default(&self) -> i32 { + self.monthly_revenue_usd.unwrap_or(0) + } + + /// Get last updated with default + pub fn last_updated_or_default(&self) -> String { + self.last_updated.clone().unwrap_or_else(|| "Unknown".to_string()) + } + + /// Get auto healing with default + pub fn auto_healing_or_default(&self) -> bool { + self.auto_healing.unwrap_or(false) + } +} + +pub struct Deployment { + pub base_data: BaseModelData, + pub app_id: String, + pub instance_id: String, + pub status: String, + pub region: String, + pub health_score: Option, + pub resource_usage: Option, + pub deployed_at: Option>, +} + +/// Resource utilization information +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ResourceUtilization { + pub cpu: i32, + pub memory: i32, + pub storage: i32, + pub network: i32, +} + +/// Deployment status enumeration +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub enum DeploymentStatus { + #[default] + Running, + Stopped, + Failed, + Pending, + Maintenance, +} + +/// Unified App builder +#[derive(Default)] +pub struct AppBuilder { + base_data: BaseModelData, + name: Option, + category: Option, + version: Option, + status: Option, + customer_name: Option, + customer_email: Option, + deployed_date: Option, + health_score: Option, + region: Option, + instances: Option, + resource_usage: Option, + deployments: Option, + rating: Option, + monthly_revenue_usd: Option, + cost_per_month: Option, + last_updated: Option, + auto_healing: Option, + provider: Option, + deployed_at: Option>, +} + +impl AppBuilder { + pub fn new() -> Self { + Self { + base_data: BaseModelData::new(), + ..Default::default() + } + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn category(mut self, category: impl Into) -> Self { + self.category = Some(category.into()); + self + } + + pub fn version(mut self, version: impl Into) -> Self { + self.version = Some(version.into()); + self + } + + pub fn status(mut self, status: impl Into) -> Self { + self.status = Some(status.into()); + self + } + + pub fn customer_name(mut self, name: impl Into) -> Self { + self.customer_name = Some(name.into()); + self + } + + pub fn customer_email(mut self, email: impl Into) -> Self { + self.customer_email = Some(email.into()); + self + } + + pub fn deployed_date(mut self, date: impl Into) -> Self { + self.deployed_date = Some(date.into()); + self + } + + pub fn health_score(mut self, score: f32) -> Self { + self.health_score = Some(score); + self + } + + pub fn region(mut self, region: impl Into) -> Self { + self.region = Some(region.into()); + self + } + + pub fn instances(mut self, instances: i32) -> Self { + self.instances = Some(instances); + self + } + + pub fn resource_usage(mut self, usage: ResourceUtilization) -> Self { + self.resource_usage = Some(usage); + self + } + + pub fn deployments(mut self, deployments: i32) -> Self { + self.deployments = Some(deployments); + self + } + + pub fn rating(mut self, rating: f32) -> Self { + self.rating = Some(rating); + self + } + + pub fn monthly_revenue_usd(mut self, revenue: i32) -> Self { + self.monthly_revenue_usd = Some(revenue); + self + } + + pub fn cost_per_month(mut self, cost: Decimal) -> Self { + self.cost_per_month = Some(cost); + self + } + + pub fn last_updated(mut self, updated: impl Into) -> Self { + self.last_updated = Some(updated.into()); + self + } + + pub fn auto_healing(mut self, enabled: bool) -> Self { + self.auto_healing = Some(enabled); + self + } + + pub fn provider(mut self, provider: impl Into) -> Self { + self.provider = Some(provider.into()); + self + } + + pub fn deployed_at(mut self, date: DateTime) -> Self { + self.deployed_at = Some(date); + self + } + + pub fn build(self) -> Result { + Ok(App { + base_data: self.base_data, + name: self.name.ok_or("name is required")?, + category: self.category, + version: self.version, + status: self.status.unwrap_or_else(|| "Active".to_string()), + customer_name: self.customer_name, + customer_email: self.customer_email, + deployed_date: self.deployed_date, + health_score: self.health_score, + region: self.region, + instances: self.instances, + resource_usage: self.resource_usage, + deployments: self.deployments, + rating: self.rating, + monthly_revenue_usd: self.monthly_revenue_usd, + cost_per_month: self.cost_per_month, + last_updated: self.last_updated, + auto_healing: self.auto_healing, + provider: self.provider, + deployed_at: self.deployed_at, + }) + } +} + +impl App { + pub fn builder() -> AppBuilder { + AppBuilder::new() + } + + // Template methods for common app types + pub fn analytics_template(name: &str) -> Self { + Self::builder() + .name(name) + .category("Analytics") + .version("1.0.0") + .status("Active") + .rating(4.5) + .auto_healing(true) + .build() + .unwrap() + } + + pub fn database_template(name: &str) -> Self { + Self::builder() + .name(name) + .category("Database") + .version("1.0.0") + .status("Active") + .rating(4.2) + .auto_healing(false) // Databases need manual intervention + .build() + .unwrap() + } + + pub fn web_template(name: &str) -> Self { + Self::builder() + .name(name) + .category("Web") + .version("1.0.0") + .status("Active") + .rating(4.0) + .auto_healing(true) + .build() + .unwrap() + } + + // Fluent methods for chaining + pub fn with_stats(mut self, deployments: i32, rating: f32, monthly_revenue_usd: i32) -> Self { + self.deployments = Some(deployments); + self.rating = Some(rating); + self.monthly_revenue_usd = Some(monthly_revenue_usd); + self + } + + pub fn with_auto_healing(mut self, enabled: bool) -> Self { + self.auto_healing = Some(enabled); + self + } + + pub fn with_version(mut self, version: impl Into) -> Self { + self.version = Some(version.into()); + self + } + + pub fn with_last_updated(mut self, updated: impl Into) -> Self { + self.last_updated = Some(updated.into()); + self + } + + pub fn with_deployment_info(mut self, customer_name: &str, customer_email: &str, region: &str) -> Self { + self.customer_name = Some(customer_name.to_string()); + self.customer_email = Some(customer_email.to_string()); + self.region = Some(region.to_string()); + self.deployed_at = Some(Utc::now()); + self + } + + pub fn with_resource_usage(mut self, cpu: i32, memory: i32, storage: i32, network: i32) -> Self { + self.resource_usage = Some(ResourceUtilization { + cpu, + memory, + storage, + network, + }); + self + } +} + +// Type aliases for backward compatibility +pub type PublishedApp = App; +pub type AppDeployment = App; +pub type DeploymentStat = App; +pub type UserDeployment = App; + +pub type PublishedAppBuilder = AppBuilder; +pub type AppDeploymentBuilder = AppBuilder; +pub type DeploymentStatBuilder = AppBuilder; +pub type UserDeploymentBuilder = AppBuilder; diff --git a/heromodels/src/models/tfmarketplace/builders.rs b/heromodels/src/models/tfmarketplace/builders.rs new file mode 100644 index 0000000..f6a912d --- /dev/null +++ b/heromodels/src/models/tfmarketplace/builders.rs @@ -0,0 +1,351 @@ +//! Builder patterns for all marketplace models +//! This module provides a centralized, maintainable way to construct complex structs +//! with sensible defaults and validation. + +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use serde_json::Value; +use std::collections::HashMap; + +use super::{ + user::{PublishedApp, DeploymentStat, ResourceUtilization, User, UserRole, MockUserData, ServiceBooking}, + product::{Product, ProductAttribute, ProductAvailability, ProductMetadata}, + order::{Order, OrderItem, OrderStatus, PaymentDetails, Address, PurchaseType}, +}; +use crate::services::user_persistence::AppDeployment; +use heromodels_core::BaseModelData; + +// ============================================================================= +// USER MODEL BUILDERS +// ============================================================================= + + + + + +#[derive(Default)] +pub struct MockDataBuilder { + user_type: Option, + include_farmer_data: Option, + include_service_data: Option, + include_app_data: Option, +} + +impl MockDataBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn user_type(mut self, user_type: impl Into) -> Self { + self.user_type = Some(user_type.into()); + self + } + + pub fn include_farmer_data(mut self, include: bool) -> Self { + self.include_farmer_data = Some(include); + self + } + + pub fn include_service_data(mut self, include: bool) -> Self { + self.include_service_data = Some(include); + self + } + + pub fn include_app_data(mut self, include: bool) -> Self { + self.include_app_data = Some(include); + self + } + + pub fn build(self) -> crate::models::user::MockUserData { + // This would create appropriate mock data based on configuration + // For now, return a default instance + crate::models::user::MockUserData::new_user() + } +} +// ============================================================================= +// FARMER DATA BUILDER +// ============================================================================= + +#[derive(Default)] +pub struct FarmerDataBuilder { + total_nodes: Option, + online_nodes: Option, + total_capacity: Option, + used_capacity: Option, + monthly_earnings: Option, + total_earnings: Option, + uptime_percentage: Option, + nodes: Option>, + earnings_history: Option>, + active_slices: Option, +} + +impl FarmerDataBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn total_nodes(mut self, total_nodes: i32) -> Self { + self.total_nodes = Some(total_nodes); + self + } + + pub fn online_nodes(mut self, online_nodes: i32) -> Self { + self.online_nodes = Some(online_nodes); + self + } + + pub fn total_capacity(mut self, capacity: crate::models::user::NodeCapacity) -> Self { + self.total_capacity = Some(capacity); + self + } + + pub fn used_capacity(mut self, capacity: crate::models::user::NodeCapacity) -> Self { + self.used_capacity = Some(capacity); + self + } + + pub fn monthly_earnings_usd(mut self, earnings: i32) -> Self { + self.monthly_earnings = Some(earnings); + self + } + + pub fn total_earnings_usd(mut self, earnings: i32) -> Self { + self.total_earnings = Some(earnings); + self + } + + pub fn uptime_percentage(mut self, uptime: f32) -> Self { + self.uptime_percentage = Some(uptime); + self + } + + pub fn nodes(mut self, nodes: Vec) -> Self { + self.nodes = Some(nodes); + self + } + + pub fn earnings_history(mut self, history: Vec) -> Self { + self.earnings_history = Some(history); + self + } + + pub fn earnings(mut self, earnings: Vec) -> Self { + self.earnings_history = Some(earnings); + self + } + + pub fn active_slices(mut self, active_slices: i32) -> Self { + self.active_slices = Some(active_slices); + self + } + + pub fn calculate_totals(mut self) -> Self { + // Calculate totals from existing data + if let Some(ref nodes) = self.nodes { + self.total_nodes = Some(nodes.len() as i32); + self.online_nodes = Some(nodes.iter().filter(|n| matches!(n.status, crate::models::user::NodeStatus::Online)).count() as i32); + + // Calculate total and used capacity from all nodes + let mut total_capacity = crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }; + + let mut used_capacity = crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }; + + for node in nodes { + total_capacity.cpu_cores += node.capacity.cpu_cores; + total_capacity.memory_gb += node.capacity.memory_gb; + total_capacity.storage_gb += node.capacity.storage_gb; + total_capacity.bandwidth_mbps += node.capacity.bandwidth_mbps; + total_capacity.ssd_storage_gb += node.capacity.ssd_storage_gb; + total_capacity.hdd_storage_gb += node.capacity.hdd_storage_gb; + + used_capacity.cpu_cores += node.used_capacity.cpu_cores; + used_capacity.memory_gb += node.used_capacity.memory_gb; + used_capacity.storage_gb += node.used_capacity.storage_gb; + used_capacity.bandwidth_mbps += node.used_capacity.bandwidth_mbps; + used_capacity.ssd_storage_gb += node.used_capacity.ssd_storage_gb; + used_capacity.hdd_storage_gb += node.used_capacity.hdd_storage_gb; + } + + self.total_capacity = Some(total_capacity); + self.used_capacity = Some(used_capacity); + + // Calculate uptime percentage + if !nodes.is_empty() { + let avg_uptime = nodes.iter().map(|n| n.uptime_percentage).sum::() / nodes.len() as f32; + self.uptime_percentage = Some(avg_uptime); + } + } + + if let Some(ref earnings) = self.earnings_history { + let total: i32 = earnings.iter().map(|e| e.amount.to_string().parse::().unwrap_or(0)).sum(); + self.total_earnings = Some(total); + self.monthly_earnings = Some(total); // Set monthly earnings as well + } + + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::FarmerData { + total_nodes: self.total_nodes.unwrap_or(0), + online_nodes: self.online_nodes.unwrap_or(0), + total_capacity: self.total_capacity.unwrap_or(crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }), + used_capacity: self.used_capacity.unwrap_or(crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }), + monthly_earnings_usd: self.monthly_earnings.unwrap_or(0), + total_earnings_usd: self.total_earnings.unwrap_or(0), + uptime_percentage: self.uptime_percentage.unwrap_or(0.0), + nodes: self.nodes.unwrap_or_default(), + earnings_history: self.earnings_history.unwrap_or_default(), + slice_templates: Vec::default(), // Will be populated separately + active_slices: self.active_slices.unwrap_or(0), + }) + } +} + +// ============================================================================= +// SERVICE BOOKING BUILDER +// ============================================================================= + +#[derive(Default)] +pub struct SpendingRecordBuilder { + date: Option, + amount: Option, + service_name: Option, + provider_name: Option, +} + +impl SpendingRecordBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn date(mut self, date: &str) -> Self { + self.date = Some(date.to_string()); + self + } + + pub fn amount(mut self, amount: i32) -> Self { + self.amount = Some(amount); + self + } + + pub fn service_name(mut self, name: &str) -> Self { + self.service_name = Some(name.to_string()); + self + } + + pub fn provider_name(mut self, name: &str) -> Self { + self.provider_name = Some(name.to_string()); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::SpendingRecord { + date: self.date.ok_or("Date is required")?, + amount: self.amount.unwrap_or(0), + service_name: self.service_name.ok_or("Service name is required")?, + provider_name: self.provider_name.ok_or("Provider name is required")?, + }) + } +} + +impl crate::models::user::SpendingRecord { + pub fn builder() -> SpendingRecordBuilder { + SpendingRecordBuilder::new() + } +} + +// ============================================================================= +// AUTO TOP-UP BUILDERS +// ============================================================================= + +#[derive(Default)] +pub struct AutoTopUpSettingsBuilder { + enabled: Option, + threshold_amount: Option, + topup_amount: Option, + payment_method_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + daily_limit: Option, + monthly_limit: Option, +} + +impl AutoTopUpSettingsBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = Some(enabled); + self + } + + pub fn threshold_amount(mut self, amount: Decimal) -> Self { + self.threshold_amount = Some(amount); + self + } + + pub fn topup_amount(mut self, amount: Decimal) -> Self { + self.topup_amount = Some(amount); + self + } + + pub fn payment_method_id(mut self) -> Self{ + self.payment_method_id = Some(id.into()); + self + } + + pub fn daily_limit(mut self, limit: Decimal) -> Self { + self.daily_limit = Some(limit); + self + } + + pub fn monthly_limit(mut self, limit: Decimal) -> Self { + self.monthly_limit = Some(limit); + self + } + + pub fn build(self) -> Result { + Ok(crate::services::user_persistence::AutoTopUpSettings { + enabled: self.enabled.unwrap_or(false), + threshold_amount_usd: self.threshold_amount.unwrap_or(dec!(10.0)), + topup_amount_usd: self.topup_amount.unwrap_or(dec!(25.0)), + payment_method_base_data: BaseModelData::new(), + // id: self.payment_method_id.ok_or("payment_method_id is required")? - moved to base_data, + daily_limit_usd: self.daily_limit, + monthly_limit_usd: self.monthly_limit, + // created_at: chrono::Utc::now() - moved to base_data, + // updated_at: chrono::Utc::now() - moved to base_data, + }) + } +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/cart.rs b/heromodels/src/models/tfmarketplace/cart.rs new file mode 100644 index 0000000..098cd85 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/cart.rs @@ -0,0 +1,105 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use crate::models::tfmarketplace::user::ResourceUtilization; + +/// Shopping Cart Models +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CartItem { + pub product_id: u32, + pub quantity: u32, + pub selected_specifications: HashMap, + pub added_at: DateTime, + +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Cart { + pub base_data: BaseModelData, + pub items: Vec, +} + +impl Cart { + pub fn new() -> Self{ + let now = Utc::now(); + Self { + base_data: BaseModelData::new(), + items: Vec::default(), + } + } + + pub fn add_item(&mut self, item: CartItem) { + // Check if item already exists and update quantity + if let Some(existing_item) = self.items.iter_mut() + .find(|i| i.product_id == item.product_id && i.selected_specifications == item.selected_specifications) { + existing_item.quantity += item.quantity; + } else { + self.items.push(item); + } + } + + pub fn remove_item(&mut self, product_id: &str, name: &str) -> bool{ + let initial_len = self.items.len(); + self.items.retain(|item| item.product_id != product_id); + if self.items.len() != initial_len { + self.base_data.updated_at = Utc::now(); + true + } else { + false + } + } + + pub fn update_item_quantity(&mut self, product_id: &str, name: &str) -> bool { + if let Some(item) = self.items.iter_mut().find(|i| i.product_id == product_id) { + if quantity == 0 { + return self.remove_item(product_id); + } + item.quantity = quantity; + item.updated_at = Utc::now(); + self.base_data.updated_at = Utc::now(); + true + } else { + false + } + } + + pub fn clear(&mut self) { + self.items.clear(); + self.base_data.updated_at = Utc::now(); + } + + pub fn get_total_items(&self) -> u32 { + self.items.iter().map(|item| item.quantity).sum() + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +impl CartItem { + pub fn new(product_id: &str, name: &str) -> Self { + let now = Utc::now(); + Self { + product_id, + quantity, + selected_specifications: HashMap::default(), + added_at: now, + // updated_at: now - moved to base_data, + } + } + + pub fn with_specifications( + product_id: &str, name: &str) -> Self { + let now = Utc::now(); + Self { + product_id, + quantity, + selected_specifications: specifications, + added_at: now, + // updated_at: now - moved to base_data, + } + } +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/currency.rs b/heromodels/src/models/tfmarketplace/currency.rs new file mode 100644 index 0000000..e1fe5f7 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/currency.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::CustomType; +use crate::models::tfmarketplace::user::ResourceUtilization; + +/// Configurable currency support for any currency type +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +pub struct Currency { + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + #[index] + pub code: String, // USD, EUR, BTC, ETH, etc. + pub name: String, + pub symbol: String, + pub currency_type: CurrencyType, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum CurrencyType { + Fiat, + Cryptocurrency, + Token, + Points, // For loyalty/reward systems + Custom(String), // For marketplace-specific currencies +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Price { + pub base_amount: Decimal, // Amount in marketplace base currency + pub base_currency: String, + pub display_currency: String, + pub display_amount: Decimal, + pub formatted_display: String, + pub conversion_rate: Decimal, + pub conversion_timestamp: DateTime, +} + +impl Currency { + pub fn new( + code: String, + name: String, + symbol: String, + currency_type: CurrencyType, + ) -> Self { + Self { + base_data: BaseModelData::new(), + code, + name, + symbol, + currency_type, + } + } +} + +impl Price { + pub fn new( + base_amount: Decimal, + base_currency: String, + display_currency: String, + conversion_rate: Decimal, + ) -> Self { + let display_amount = base_amount * conversion_rate; + // Use proper currency symbol formatting - this will be updated by the currency service + Self { + base_amount, + base_currency: base_currency.clone(), + display_currency: display_currency.clone(), + display_amount, + formatted_display: format!("{} {}", display_amount.round_dp(2), display_currency), + conversion_rate, + conversion_timestamp: Utc::now(), + } + } + + pub fn format_with_symbol(&self, symbol: &str) -> String { + format!("{} {}", + self.display_amount.round_dp(2), + symbol + ) + } + + pub fn update_formatted_display(&mut self, formatted: String) { + self.formatted_display = formatted; + } +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/farmer.rs b/heromodels/src/models/tfmarketplace/farmer.rs new file mode 100644 index 0000000..db12577 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/farmer.rs @@ -0,0 +1,30 @@ + +/// Farmer-specific data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmerData { + pub total_nodes: i32, + pub online_nodes: i32, + pub total_capacity: NodeCapacity, + pub used_capacity: NodeCapacity, + pub monthly_earnings_usd: i32, + pub total_earnings_usd: i32, + pub uptime_percentage: f32, + pub nodes: Vec, + pub earnings_history: Vec, + pub slice_templates: Vec, + pub active_slices: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmerSettings { + #[serde(default)] + pub auto_accept_deployments: bool, + #[serde(default = "default_maintenance_window")] + pub maintenance_window: String, + #[serde(default)] + pub notification_preferences: NotificationSettings, + pub minimum_deployment_duration: i32, // hours + pub preferred_regions: Vec, + #[serde(default)] + pub default_slice_customizations: Option>, // Placeholder for DefaultSliceFormat +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/mod.rs b/heromodels/src/models/tfmarketplace/mod.rs new file mode 100644 index 0000000..a8e4236 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/mod.rs @@ -0,0 +1,17 @@ +// Export models - starting with basic models first +// pub mod user; +// pub mod product; +// pub mod currency; +// pub mod order; +// pub mod pool; +// pub mod builders; // Re-enabled with essential builders only +// pub mod cart; +// pub mod payment; +// pub mod service; +// pub mod slice; +// pub mod node; +pub mod app; + +// Re-export commonly used types for easier access +pub use app::{App, PublishedApp, PublishedAppBuilder, ResourceUtilization, AppBuilder, DeploymentStatus}; +// pub mod node; // Temporarily disabled - has many service dependencies diff --git a/heromodels/src/models/tfmarketplace/node.rs b/heromodels/src/models/tfmarketplace/node.rs new file mode 100644 index 0000000..4271161 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/node.rs @@ -0,0 +1,1660 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize, Deserializer}; +use rust_decimal::Decimal; +use std::str::FromStr; +use heromodels_core::BaseModelData; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeCapacity { + pub cpu_cores: i32, + pub memory_gb: i32, + pub storage_gb: i32, // Keep for backward compatibility + pub bandwidth_mbps: i32, + // Enhanced storage breakdown for GridProxy integration + #[serde(default)] + pub ssd_storage_gb: i32, // SSD storage in GB + #[serde(default)] + pub hdd_storage_gb: i32, // HDD storage in GB +} + +/// Enhanced Node structure for farmer dashboard with modern types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Node { + pub base_data: BaseModelData, + pub name: String, + pub location: String, + #[serde(deserialize_with = "deserialize_node_status")] + pub status: NodeStatus, + pub capacity: NodeCapacity, + pub used_capacity: NodeCapacity, + pub uptime_percentage: f32, + #[serde(deserialize_with = "deserialize_earnings", alias = "earnings_today")] + pub earnings_today_usd: rust_decimal::Decimal, + #[serde(deserialize_with = "deserialize_datetime")] + pub last_seen: DateTime, + pub health_score: f32, + pub region: String, + pub node_type: String, // "3Node", "Gateway", "Compute" + #[serde(default)] + pub slice_formats: Option>, // Allocated slice format IDs (DEPRECATED) + /// Node rental configuration options + #[serde(default)] + pub rental_options: Option, + /// Current rental availability status + pub availability_status: NodeAvailabilityStatus, + pub grid_node_id: u32, + /// Data fetched from ThreeFold Grid + #[serde(default)] + pub grid_data: Option, + /// Node group this node belongs to + pub node_group_id: u32, + /// When the node was assigned to its current group + pub group_assignment_date: Option>, + /// Shared slice format when in a group (legacy - will be removed) + pub group_slice_format: Option, + /// Shared slice price when in a group (legacy - will be removed) + pub group_slice_price: Option, + /// Node staking configuration options + pub staking_options: Option, + /// Marketplace SLA - what the farmer promises to customers (separate from grid reality) + pub marketplace_sla: Option, + /// Total number of base slices this node can provide + pub total_base_slices: u32, + /// Number of base slices currently allocated/rented + #[serde(default)] + pub allocated_base_slices: u32, + /// Current slice allocations (active rentals) + #[serde(default)] + pub slice_allocations: Vec, // Placeholder for SliceAllocation + /// Available slice combinations for rental + #[serde(default)] + pub available_combinations: Vec, // Placeholder for SliceCombination + /// Slice pricing configuration + #[serde(default)] + pub slice_pricing: serde_json::Value, // Placeholder for SlicePricing + /// When slice data was last calculated + #[serde(default)] + pub slice_last_calculated: Option>, +} + +// Custom deserializer for NodeStatus from string +fn deserialize_node_status<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.as_str() { + "Online" => Ok(NodeStatus::Online), + "Offline" => Ok(NodeStatus::Offline), + "Maintenance" => Ok(NodeStatus::Maintenance), + "Error" => Ok(NodeStatus::Error), + "Standby" => Ok(NodeStatus::Standby), + _ => Ok(NodeStatus::Online), // Default fallback + } +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EarningsRecord { + pub date: String, + pub amount: i32, + pub source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeStatus { + Online, + Offline, + Maintenance, + Error, + Standby, +} + +impl std::fmt::Display for NodeStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeStatus::Online => write!(f, "Online"), + NodeStatus::Offline => write!(f, "Offline"), + NodeStatus::Maintenance => write!(f, "Maintenance"), + NodeStatus::Error => write!(f, "Error"), + NodeStatus::Standby => write!(f, "Standby"), + } + } +} + +fn default_maintenance_window() -> String { + "02:00-04:00 UTC".to_string() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationSettings { + #[serde(alias = "email", default)] + pub email_enabled: bool, + #[serde(alias = "sms", default)] + pub sms_enabled: bool, + #[serde(default)] + pub push: Option, + #[serde(default)] + pub node_offline_alerts: Option, + #[serde(default)] + pub earnings_reports: Option, + #[serde(default)] + pub maintenance_reminders: Option, +} + +impl Default for NotificationSettings { + fn default() -> Self { + Self { + email_enabled: true, + sms_enabled: false, + push: Some(true), + node_offline_alerts: Some(true), + earnings_reports: Some(true), + maintenance_reminders: Some(true), + } + } +} + +/// Marketplace SLA configuration - what the farmer promises to customers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketplaceSLA { + /// Uptime guarantee percentage (e.g., 99.95%) + pub uptime_guarantee_percentage: f32, + /// Bandwidth guarantee in Mbps (e.g., 123) + pub bandwidth_guarantee_mbps: i32, + /// Base slice price in USD per hour (e.g., 0.53) + pub base_slice_price: rust_decimal::Decimal, + /// When this SLA was last updated + pub last_updated: chrono::DateTime, +} + +impl Default for MarketplaceSLA { + fn default() -> Self { + Self { + uptime_guarantee_percentage: 99.0, + bandwidth_guarantee_mbps: 100, + base_slice_price: rust_decimal::Decimal::new(50, 2), // $0.50 USD + last_updated: chrono::Utc::now(), + } + } +} + +/// Node rental options that farmers can configure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeRentalOptions { + /// Whether slice rentals are allowed + pub slice_rental_enabled: bool, + /// Whether full node rentals are allowed + pub full_node_rental_enabled: bool, + /// Full node rental pricing configuration + pub full_node_pricing: Option, + /// Minimum rental duration in days + pub minimum_rental_days: u32, + /// Maximum rental duration in days (None = unlimited) + pub maximum_rental_days: Option, + /// Auto-renewal settings + pub auto_renewal_enabled: bool, +} + +impl Default for NodeRentalOptions { + fn default() -> Self { + Self { + slice_rental_enabled: true, + full_node_rental_enabled: false, + full_node_pricing: None, + minimum_rental_days: 30, + maximum_rental_days: None, + auto_renewal_enabled: false, + } + } +} + +/// Node staking options that farmers can configure +/// Staking is used for slashing protection, not for discounts +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeStakingOptions { + /// Whether staking is enabled for this node + pub staking_enabled: bool, + /// Amount of USD Credits staked on this node + pub staked_amount: Decimal, + /// When staking started + pub staking_start_date: Option>, + /// Staking period in months (3, 6, 12, 24) + pub staking_period_months: u32, + /// Whether staking can be withdrawn early (with penalty) + pub early_withdrawal_allowed: bool, + /// Early withdrawal penalty percentage (0-50) + pub early_withdrawal_penalty_percent: f32, +} + +impl Default for NodeStakingOptions { + fn default() -> Self { + Self { + staking_enabled: false, + staked_amount: Decimal::ZERO, + staking_start_date: None, + staking_period_months: 12, + early_withdrawal_allowed: true, + early_withdrawal_penalty_percent: 25.0, + } + } +} + +impl NodeStakingOptions { + /// Create a new builder for NodeStakingOptions + pub fn builder() -> crate::models::tfmarketplace::builders::NodeStakingOptionsBuilder { + crate::models::tfmarketplace::builders::NodeStakingOptionsBuilder::new() + } + + /// Check if staking period has ended + pub fn is_staking_period_ended(&self) -> bool { + if let Some(start_date) = self.staking_start_date { + let end_date = start_date + chrono::Duration::days((self.staking_period_months * 30) as i64); + Utc::now() >= end_date + } else { + false + } + } + + /// Get remaining days in staking period + pub fn days_remaining(&self) -> i64 { + if let Some(start_date) = self.staking_start_date { + let end_date = start_date + chrono::Duration::days((self.staking_period_months * 30) as i64); + (end_date - Utc::now()).num_days().max(0) + } else { + 0 + } + } + + /// Calculate early withdrawal penalty amount + pub fn calculate_early_withdrawal_penalty(&self) -> Decimal { + if self.early_withdrawal_allowed && !self.is_staking_period_ended() { + self.staked_amount * Decimal::from_f32_retain(self.early_withdrawal_penalty_percent).unwrap_or(Decimal::ZERO) / Decimal::from(100) + } else { + Decimal::ZERO + } + } +} + +/// Full node rental pricing configuration with auto-calculation support +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FullNodePricing { + /// Hourly rate in USD + pub hourly: Decimal, + /// Daily rate in USD + pub daily: Decimal, + /// Monthly rate in USD + pub monthly: Decimal, + /// Yearly rate in USD + pub yearly: Decimal, + /// Whether to auto-calculate rates from hourly base + pub auto_calculate: bool, + /// Daily discount percentage (0-50) + pub daily_discount_percent: f32, + /// Monthly discount percentage (0-50) + pub monthly_discount_percent: f32, + /// Yearly discount percentage (0-50) + pub yearly_discount_percent: f32, +} + +impl Default for FullNodePricing { + fn default() -> Self { + Self { + hourly: Decimal::from(0), + daily: Decimal::from(0), + monthly: Decimal::from(0), + yearly: Decimal::from(0), + auto_calculate: true, + daily_discount_percent: 0.0, + monthly_discount_percent: 0.0, + yearly_discount_percent: 0.0, + } + } +} + +impl FullNodePricing { + /// Create a new builder for FullNodePricing + pub fn builder() -> crate::models::tfmarketplace::builders::FullNodePricingBuilder { + crate::models::tfmarketplace::builders::FullNodePricingBuilder::new() + } + + /// Calculate all rates from hourly base rate with discounts + pub fn calculate_from_hourly(&mut self) { + if !self.auto_calculate { + return; + } + + let hourly_f64 = self.hourly.to_string().parse::().unwrap_or(0.0); + + // Calculate base rates + let base_daily = hourly_f64 * 24.0; + let base_monthly = hourly_f64 * 24.0 * 30.0; + let base_yearly = hourly_f64 * 24.0 * 365.0; + + // Apply discounts (convert f32 to f64 for calculations) + let daily_rate = base_daily * (1.0 - self.daily_discount_percent as f64 / 100.0); + let monthly_rate = base_monthly * (1.0 - self.monthly_discount_percent as f64 / 100.0); + let yearly_rate = base_yearly * (1.0 - self.yearly_discount_percent as f64 / 100.0); + + // Update rates + self.daily = Decimal::from_str(&daily_rate.to_string()).unwrap_or(Decimal::from(0)); + self.monthly = Decimal::from_str(&monthly_rate.to_string()).unwrap_or(Decimal::from(0)); + self.yearly = Decimal::from_str(&yearly_rate.to_string()).unwrap_or(Decimal::from(0)); + } +} + +impl NodeRentalOptions { + /// Create a new builder for NodeRentalOptions + pub fn builder() -> crate::models::tfmarketplace::builders::NodeRentalOptionsBuilder { + crate::models::tfmarketplace::builders::NodeRentalOptionsBuilder::new() + } +} + +/// Node availability status for rental management +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum NodeAvailabilityStatus { + /// Node is fully available for any rental type + Available, + /// Node has some slices rented but capacity remains + PartiallyRented, + /// Node is fully rented (either full node or all slices) + FullyRented, + /// Node is offline or in maintenance + Unavailable, + /// Node is reserved for specific customer + Reserved, +} + +impl Default for NodeAvailabilityStatus { + fn default() -> Self { + Self::Available + } +} + +/// Individual node rental record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeRental { + pub base_data: BaseModelData::new(), + pub node_id: u32, + pub renter_id: u32, + pub rental_type: NodeRentalType, + pub monthly_cost: Decimal, + pub start_date: DateTime, + pub end_date: DateTime, + pub status: NodeRentalStatus, + pub auto_renewal: bool, + pub payment_method: String, + pub metadata: std::collections::HashMap, +} + +/// Type of node rental +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeRentalType { + /// Renting specific slices of the node + Slice { + slice_ids: Vec, + total_cpu_cores: u32, + total_memory_gb: u32, + total_storage_gb: u32, + }, + /// Renting the entire node exclusively + FullNode, +} + +/// Status of a node rental +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeRentalStatus { + Active, + Pending, + Expired, + Cancelled, + Suspended, +} + +/// Farmer earnings from node rentals +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmerRentalEarning { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub node_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub rental_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub renter_id: u32, + pub amount: Decimal, + pub currency: String, + pub earning_date: DateTime, + pub rental_type: NodeRentalType, + pub payment_status: PaymentStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentStatus { + Pending, + Completed, + Failed, + Refunded, +} + +impl NodeRental { + /// Create a new builder for NodeRental + pub fn builder() -> crate::models::tfmarketplace::builders::NodeRentalBuilder { + crate::models::tfmarketplace::builders::NodeRentalBuilder::new() + } + + /// Check if rental is currently active + pub fn is_active(&self) -> bool { + matches!(self.status, NodeRentalStatus::Active) && + Utc::now() >= self.start_date && + Utc::now() <= self.end_date + } + + /// Get remaining days in rental + pub fn days_remaining(&self) -> i64 { + (self.end_date - Utc::now()).num_days() + } + + /// Calculate total rental cost + pub fn total_cost(&self) -> Decimal { + let months = (self.end_date - self.start_date).num_days() as f64 / 30.0; + self.monthly_cost * Decimal::try_from(months).unwrap_or(Decimal::ZERO) + } +} + +impl FarmerRentalEarning { + /// Create a new builder for FarmerRentalEarning + pub fn builder() -> crate::models::tfmarketplace::builders::FarmerRentalEarningBuilder { + crate::models::tfmarketplace::builders::FarmerRentalEarningBuilder::new() + } +} + +impl FarmNode { + /// Create a new builder for FarmNode + pub fn builder() -> crate::models::tfmarketplace::builders::FarmNodeBuilder { + crate::models::tfmarketplace::builders::FarmNodeBuilder::new() + } +} + +impl NodeGroup { + /// Create a new builder for NodeGroup + pub fn builder() -> crate::models::tfmarketplace::builders::NodeGroupBuilder { + crate::models::tfmarketplace::builders::NodeGroupBuilder::new() + } +} + +impl GridNodeData { + /// Create a new builder for GridNodeData + pub fn builder() -> crate::models::tfmarketplace::builders::GridNodeDataBuilder { + crate::models::tfmarketplace::builders::GridNodeDataBuilder::new() + } +} + + +/// Enhanced User Statistics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UsageStatistics { + #[serde(default)] + pub total_deployments: i32, + #[serde(default)] + pub active_services: i32, + #[serde(default)] + pub total_spent: rust_decimal::Decimal, + #[serde(default)] + pub favorite_categories: Vec, + #[serde(default)] + pub usage_trends: Vec, + #[serde(default = "default_login_frequency")] + pub login_frequency: f32, // logins per week + #[serde(default)] + pub preferred_regions: Vec, + #[serde(default)] + pub account_age_days: i32, + #[serde(deserialize_with = "deserialize_datetime_optional", default = "default_last_activity")] + pub last_activity: DateTime, +} + +fn default_login_frequency() -> f32 { + 1.0 +} + +fn default_last_activity() -> DateTime { + Utc::now() +} + +impl Default for UsageStatistics { + fn default() -> Self { + Self { + total_deployments: 0, + active_services: 0, + total_spent: rust_decimal::Decimal::ZERO, + favorite_categories: Vec::new(), + usage_trends: Vec::new(), + login_frequency: 1.0, + preferred_regions: Vec::new(), + account_age_days: 0, + last_activity: Utc::now(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UsageTrend { + pub period: String, // "week", "month", "quarter" + pub metric: String, // "deployments", "spending", "logins" + pub value: f32, + pub change_percentage: f32, +} + +/// ThreeFold Grid Node Data fetched from gridproxy/graphql +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GridNodeData { + pub grid_node_base_data: BaseModelData::new(), + // id: u32 - moved to base_data, + pub city: Option, + pub country: Option, + pub farm_name: Option, + pub farm_base_data: BaseModelData::new(), + // id: u32 - moved to base_data, + pub public_ips: u32, + pub total_resources: NodeCapacity, + pub used_resources: NodeCapacity, + pub certification_type: String, // "Certified", "DIY" + pub farming_policy_base_data: BaseModelData::new(), + // id: u32 - moved to base_data, + #[serde(deserialize_with = "deserialize_datetime")] + pub last_updated: DateTime, +} + +/// Node Group for managing multiple nodes together +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeGroup { + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + // Additional fields would go here +} + +/// Type of node group - default or custom +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeGroupType { + Default(DefaultGroupType), + Custom, +} + +/// Default group types provided by the system +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DefaultGroupType { + Compute, // group_1 - Compute Group + Storage, // group_2 - Storage Group + AiGpu, // group_3 - AI/GPU Group +} + +/// Configuration for node groups +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeGroupConfig { + pub preferred_slice_formats: Vec, + pub default_pricing: Option>, + pub resource_optimization: ResourceOptimization, + pub auto_scaling: bool, +} + +/// Resource optimization settings for groups +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ResourceOptimization { + Balanced, + Performance, + Efficiency, + Custom, +} + +impl Default for NodeGroupConfig { + fn default() -> Self { + Self { + preferred_slice_formats: vec!["basic".to_string(), "standard".to_string()], + default_pricing: None, + resource_optimization: ResourceOptimization::Balanced, + auto_scaling: false, + } + } +} + +impl DefaultGroupType { + pub fn get_id(&self) -> &'static str { + match self { + DefaultGroupType::Compute => "group_1", + DefaultGroupType::Storage => "group_2", + DefaultGroupType::AiGpu => "group_3", + } + } + + pub fn get_name(&self) -> &'static str { + match self { + DefaultGroupType::Compute => "Compute", + DefaultGroupType::Storage => "Storage", + DefaultGroupType::AiGpu => "AI/GPU", + } + } + + pub fn get_description(&self) -> &'static str { + match self { + DefaultGroupType::Compute => "General purpose compute nodes for applications and services", + DefaultGroupType::Storage => "Storage-optimized nodes for data storage and backup", + DefaultGroupType::AiGpu => "High-performance nodes with GPU acceleration for AI workloads", + } + } + + pub fn get_default_config(&self) -> NodeGroupConfig { + match self { + DefaultGroupType::Compute => NodeGroupConfig { + preferred_slice_formats: vec!["basic".to_string(), "standard".to_string(), "performance".to_string()], + default_pricing: None, + resource_optimization: ResourceOptimization::Balanced, + auto_scaling: true, + }, + DefaultGroupType::Storage => NodeGroupConfig { + preferred_slice_formats: vec!["basic".to_string(), "standard".to_string()], + default_pricing: None, + resource_optimization: ResourceOptimization::Efficiency, + auto_scaling: false, + }, + DefaultGroupType::AiGpu => NodeGroupConfig { + preferred_slice_formats: vec!["performance".to_string()], + default_pricing: None, + resource_optimization: ResourceOptimization::Performance, + auto_scaling: true, + }, + } + } +} + +/// Statistics for a node group +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GroupStatistics { + pub group_id: u32, + // id: String - moved to base_data, + pub total_nodes: i32, + pub online_nodes: i32, + pub total_capacity: NodeCapacity, + pub average_uptime: f32, + pub group_type: NodeGroupType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WalletSummary { + pub balance: rust_decimal::Decimal, + pub currency: String, + pub recent_transactions: i32, + pub pending_transactions: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeploymentStat { + pub app_name: String, + pub region: String, + pub instances: i32, + pub status: String, + pub resource_usage: ResourceUtilization, + #[serde(default)] + pub customer_name: Option, + #[serde(default)] + pub deployed_date: Option, + pub deployment_id: u32, + #[serde(default)] + pub auto_healing: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RevenueRecord { + pub date: String, + pub amount: i32, + pub app_name: String, +} + +/// Transaction record for wallet operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub transaction_type: TransactionType, + pub amount: rust_decimal::Decimal, + pub timestamp: DateTime, + pub status: TransactionStatus, +} + +/// Types of transactions +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TransactionType { + Purchase { product_id: String }, + Rental { product_id: String }, + Transfer { to_user: String }, + Earning { source: String }, + // Instant purchase transaction + InstantPurchase { + product_base_data: BaseModelData::new(), + // id: String - moved to base_data, + order_base_data: BaseModelData::new(), + // id: String - moved to base_data, + purchase_method: String + }, + // Pool-related transactions (now USD-based) + Exchange { + pool_base_data: BaseModelData::new(), + // id: String - moved to base_data, + from_token: String, + to_token: String, + from_amount_usd: rust_decimal::Decimal, + to_amount_usd: rust_decimal::Decimal + }, + Stake { + amount_usd: rust_decimal::Decimal, + duration_months: u32 + }, + Unstake { + amount_usd: rust_decimal::Decimal + }, + // Auto top-up transaction type (USD-based) + AutoTopUp { + triggered_by_purchase: Option, + threshold_amount_usd: rust_decimal::Decimal, + amount_usd: rust_decimal::Decimal, + payment_method: String + }, + // Credits transaction types (USD-based) + CreditsPurchase { payment_method: String }, + CreditsSale { + currency: String, + fiat_amount: rust_decimal::Decimal, + payout_method: String + }, + CreditsTransfer { to_user: String, note: Option }, +} + +/// Transaction status +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TransactionStatus { + Pending, + Completed, + Failed, + Cancelled, +} + +/// Rental record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Rental { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub product_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub start_date: DateTime, + pub end_date: Option>, + pub status: RentalStatus, + pub monthly_cost: rust_decimal::Decimal, +} + +/// Rental status +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RentalStatus { + Active, + Expired, + Cancelled, + Pending, +} + +#[derive(Default)] +pub struct FarmNodeBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + name: Option, + location: Option, + status: Option, + capacity: Option, + used_capacity: Option, + uptime_percentage: Option, + earnings_today: Option, + last_seen: Option>, + health_score: Option, + region: Option, + node_type: Option, + rental_options: Option, + availability_status: Option, + grid_node_id: Option, + grid_data: Option, + node_group_id: Option, + group_assignment_date: Option>, + group_slice_format: Option, + group_slice_price: Option, +} + +impl FarmNodeBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn location(mut self, location: impl Into) -> Self { + self.location = Some(location.into()); + self + } + + pub fn status(mut self, status: crate::models::user::NodeStatus) -> Self { + self.status = Some(status); + self + } + + pub fn capacity(mut self, capacity: crate::models::user::NodeCapacity) -> Self { + self.capacity = Some(capacity); + self + } + + pub fn used_capacity(mut self, capacity: crate::models::user::NodeCapacity) -> Self { + self.used_capacity = Some(capacity); + self + } + + pub fn uptime_percentage(mut self, uptime: f32) -> Self { + self.uptime_percentage = Some(uptime); + self + } + + pub fn earnings_today_usd(mut self, earnings: rust_decimal::Decimal) -> Self { + self.earnings_today = Some(earnings); + self + } + + pub fn grid_node_id(mut self, grid_node_id: u32) -> Self{ + self.grid_node_id = Some(grid_node_id); + self + } + + pub fn grid_data(mut self, grid_data: crate::models::user::GridNodeData) -> Self { + self.grid_data = Some(grid_data); + self + } + + pub fn node_group_id(mut self, group_id: u32) -> Self{ + self.node_group_id = Some(group_id); + self + } + + pub fn group_assignment_date(mut self, date: chrono::DateTime) -> Self { + self.group_assignment_date = Some(date); + self + } + + pub fn group_slice_format(mut self, format: impl Into) -> Self { + self.group_slice_format = Some(format.into()); + self + } + + pub fn group_slice_price(mut self, price: rust_decimal::Decimal) -> Self { + self.group_slice_price = Some(price); + self + } + + pub fn last_seen(mut self, last_seen: chrono::DateTime) -> Self { + self.last_seen = Some(last_seen); + self + } + + pub fn health_score(mut self, score: f32) -> Self { + self.health_score = Some(score); + self + } + + pub fn region(mut self, region: impl Into) -> Self { + self.region = Some(region.into()); + self + } + + pub fn node_type(mut self, node_type: impl Into) -> Self { + self.node_type = Some(node_type.into()); + self + } + + pub fn rental_options(mut self, options: crate::models::user::NodeRentalOptions) -> Self { + self.rental_options = Some(options); + self + } + + pub fn availability_status(mut self, status: crate::models::user::NodeAvailabilityStatus) -> Self { + self.availability_status = Some(status); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::FarmNode { + base_data: BaseModelData::new(), + // id: self.base_data.id.ok_or("id is required")? - moved to base_data, + name: self.name.unwrap_or_else(|| "Unnamed Node".to_string()), + location: self.location.unwrap_or_else(|| "Unknown".to_string()), + status: self.status.unwrap_or(crate::models::user::NodeStatus::Offline), + capacity: self.capacity.unwrap_or(crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }), + used_capacity: self.used_capacity.unwrap_or(crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }), + uptime_percentage: self.uptime_percentage.unwrap_or(0.0), + earnings_today_usd: self.earnings_today.unwrap_or_else(|| rust_decimal_macros::dec!(0)), + last_seen: self.last_seen.unwrap_or_else(|| chrono::Utc::now()), + health_score: self.health_score.unwrap_or(0.0), + region: self.region.unwrap_or_else(|| "Unknown".to_string()), + node_type: self.node_type.unwrap_or_else(|| "3Node".to_string()), + slice_formats: None, + rental_options: self.rental_options, + staking_options: None, + availability_status: self.availability_status.unwrap_or_default(), + grid_node_id: self.grid_node_id, + grid_data: self.grid_data, + node_group_id: self.node_group_id, + group_assignment_date: self.group_assignment_date, + group_slice_format: self.group_slice_format, + group_slice_price: self.group_slice_price, + + // NEW: Marketplace SLA field (None for builder) + marketplace_sla: None, + + total_base_slices: 0, + allocated_base_slices: 0, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: None, + }) + } +} + +// ============================================================================= +// NODE GROUP BUILDERS +// ============================================================================= + +#[derive(Default)] +pub struct NodeGroupBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + name: Option, + description: Option, + group_type: Option, + node_ids: Option>, + group_config: Option, + // created_at: Option> - moved to base_data, + // updated_at: Option> - moved to base_data, +} + +impl NodeGroupBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self) -> Self{ + self.base_data.id = Some(id.into()); + self + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn group_type(mut self, group_type: crate::models::user::NodeGroupType) -> Self { + self.group_type = Some(group_type); + self + } + + pub fn node_ids(mut self, node_ids: Vec) -> Self { + self.node_ids = Some(node_ids); + self + } + + pub fn group_config(mut self, config: crate::models::user::NodeGroupConfig) -> Self { + self.group_config = Some(config); + self + } + + /// Helper method to create a default group + pub fn default_group(mut self, default_type: crate::models::user::DefaultGroupType) -> Self { + self.base_data.id = Some(default_type.get_id().to_string()); + self.name = Some(default_type.get_name().to_string()); + self.description = Some(default_type.get_description().to_string()); + self.group_type = Some(crate::models::user::NodeGroupType::Default(default_type.clone())); + self.group_config = Some(default_type.get_default_config()); + self + } + + /// Helper method to create a custom group + pub fn custom_group(mut self, id: &str, name: &str) -> Self { + self.base_data.id = Some(id.into()); + self.name = Some(name.into()); + self.group_type = Some(crate::models::user::NodeGroupType::Custom); + self.group_config = Some(crate::models::user::NodeGroupConfig::default()); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::NodeGroup { + base_data: BaseModelData::new(), + // id: self.base_data.id.ok_or("id is required")? - moved to base_data, + name: self.name.ok_or("name is required")?, + description: self.description, + group_type: self.group_type.ok_or("group_type is required")?, + node_ids: self.node_ids.unwrap_or_default(), + group_config: self.group_config.unwrap_or_default(), + // created_at: self.base_data.created_at.unwrap_or_else(|| chrono::Utc::now()) - moved to base_data, + // updated_at: self.base_data.updated_at.unwrap_or_else(|| chrono::Utc::now()) - moved to base_data, + }) + } +} + +// ============================================================================= +// GRID NODE DATA BUILDERS +// ============================================================================= + +#[derive(Default)] +pub struct GridNodeDataBuilder { + grid_node_id: u32, + city: Option, + country: Option, + farm_name: Option, + farm_id: u32, + public_ips: Option, + total_resources: Option, + used_resources: Option, + certification_type: Option, + farming_policy_id: u32, + last_updated: Option>, +} + +impl GridNodeDataBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn grid_node_id(mut self) -> Self{ + self.grid_node_id = Some(id); + self + } + + pub fn city(mut self, city: impl Into) -> Self { + self.city = Some(city.into()); + self + } + + pub fn country(mut self, country: impl Into) -> Self { + self.country = Some(country.into()); + self + } + + pub fn farm_name(mut self, farm_name: impl Into) -> Self { + self.farm_name = Some(farm_name.into()); + self + } + + pub fn farm_id(mut self, farm_id: &str, name: &str) -> Self{ + self.farm_id = Some(farm_id); + self + } + + pub fn public_ips(mut self, public_ips: u32) -> Self { + self.public_ips = Some(public_ips); + self + } + + pub fn total_resources(mut self, resources: crate::models::user::NodeCapacity) -> Self { + self.total_resources = Some(resources); + self + } + + pub fn used_resources(mut self, resources: crate::models::user::NodeCapacity) -> Self { + self.used_resources = Some(resources); + self + } + + pub fn certification_type(mut self, cert_type: impl Into) -> Self { + self.certification_type = Some(cert_type.into()); + self + } + + pub fn farming_policy_id(mut self, policy_id: &str, name: &str) -> Self{ + self.farming_policy_id = Some(policy_id); + self + } + + pub fn last_updated(mut self, last_updated: chrono::DateTime) -> Self { + self.last_updated = Some(last_updated); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::GridNodeData { + grid_node_base_data: BaseModelData::new(), + // id: self.grid_node_id.ok_or("grid_node_id is required")? - moved to base_data, + city: self.city, + country: self.country, + farm_name: self.farm_name, + farm_base_data: BaseModelData::new(), + // id: self.farm_id.unwrap_or(0) - moved to base_data, + public_ips: self.public_ips.unwrap_or(0), + total_resources: self.total_resources.unwrap_or(crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }), + used_resources: self.used_resources.unwrap_or(crate::models::user::NodeCapacity { + cpu_cores: 0, + memory_gb: 0, + storage_gb: 0, + bandwidth_mbps: 0, + ssd_storage_gb: 0, + hdd_storage_gb: 0, + }), + certification_type: self.certification_type.unwrap_or_else(|| "DIY".to_string()), + farming_policy_base_data: BaseModelData::new(), + // id: self.farming_policy_id.unwrap_or(0) - moved to base_data, + last_updated: self.last_updated.unwrap_or_else(|| chrono::Utc::now()), + }) + } +} + +// ============================================================================= +// NODE RENTAL BUILDERS +// ============================================================================= + +#[derive(Default)] +pub struct NodeRentalBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + node_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + renter_id: Option, + rental_type: Option, + monthly_cost: Option, + start_date: Option>, + end_date: Option>, + status: Option, + auto_renewal: Option, + payment_method: Option, + metadata: HashMap, +} + +impl NodeRentalBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self) -> Self{ + self.base_data.id = Some(id.into()); + self + } + + pub fn node_id(mut self, node_id: &str, name: &str) -> Self{ + self.node_id = Some(node_id.into()); + self + } + + pub fn renter_id(mut self, renter_id: u32) -> Self { + self.renter_id = Some(renter_id); + self + } + + pub fn rental_type(mut self, rental_type: crate::models::user::NodeRentalType) -> Self { + self.rental_type = Some(rental_type); + self + } + + pub fn monthly_cost(mut self, cost: Decimal) -> Self { + self.monthly_cost = Some(cost); + self + } + + pub fn start_date(mut self, date: DateTime) -> Self { + self.start_date = Some(date); + self + } + + pub fn end_date(mut self, date: DateTime) -> Self { + self.end_date = Some(date); + self + } + + pub fn status(mut self, status: crate::models::user::NodeRentalStatus) -> Self { + self.status = Some(status); + self + } + + pub fn auto_renewal(mut self, enabled: bool) -> Self { + self.auto_renewal = Some(enabled); + self + } + + pub fn payment_method(mut self, method: impl Into) -> Self { + self.payment_method = Some(method.into()); + self + } + + pub fn metadata(mut self, key: impl Into, value: Value) -> Self { + self.metadata.insert(key.into(), value); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::NodeRental { + node_id: self.node_id.ok_or("node_id is required")?, + renter_id: self.renter_id.ok_or("renter_id is required")?, + rental_type: self.rental_type.ok_or("rental_type is required")?, + monthly_cost: self.monthly_cost.ok_or("monthly_cost is required")?, + start_date: self.start_date.unwrap_or_else(Utc::now), + end_date: self.end_date.ok_or("end_date is required")?, + status: self.status.unwrap_or(crate::models::user::NodeRentalStatus::Pending), + auto_renewal: self.auto_renewal.unwrap_or(false), + payment_method: self.payment_method.unwrap_or_else(|| "USD".to_string()), + metadata: self.metadata, + }) + } +} + +#[derive(Default)] +pub struct FarmerRentalEarningBuilder { + base_data: BaseModelData::new(), + node_id: u32, + rental_id: u32, + renter_id: Option, + amount: Option, + currency: Option, + earning_date: Option>, + rental_type: Option, + payment_status: Option, +} + +impl FarmerRentalEarningBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self) -> Self{ + self.base_data.id = Some(id.into()); + self + } + + pub fn node_id(mut self, node_id: &str, name: &str) -> Self{ + self.node_id = Some(node_id.into()); + self + } + + pub fn rental_id(mut self, rental_id: &str, name: &str) -> Self{ + self.rental_id = Some(rental_id.into()); + self + } + + pub fn renter_id(mut self, renter_id: u32) -> Self { + self.renter_id = Some(renter_id); + self + } + + pub fn amount(mut self, amount: Decimal) -> Self { + self.amount = Some(amount); + self + } + + pub fn currency(mut self, currency: impl Into) -> Self { + self.currency = Some(currency.into()); + self + } + + pub fn earning_date(mut self, date: DateTime) -> Self { + self.earning_date = Some(date); + self + } + + pub fn rental_type(mut self, rental_type: crate::models::user::NodeRentalType) -> Self { + self.rental_type = Some(rental_type); + self + } + + pub fn payment_status(mut self, status: crate::models::user::PaymentStatus) -> Self { + self.payment_status = Some(status); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::FarmerRentalEarning { + node_id: self.node_id.ok_or("node_id is required")?, + rental_id: self.rental_id.ok_or("rental_id is required")?, + renter_id: self.renter_id.ok_or("renter_id is required")?, + amount: self.amount.ok_or("amount is required")?, + currency: self.currency.unwrap_or_else(|| "USD".to_string()), + earning_date: self.earning_date.unwrap_or_else(Utc::now), + rental_type: self.rental_type.ok_or("rental_type is required")?, + payment_status: self.payment_status.unwrap_or(crate::models::user::PaymentStatus::Pending), + }) + } +} + + + +#[derive(Default)] +pub struct NodeCreationDataBuilder { + name: Option, + location: Option, + cpu_cores: Option, + memory_gb: Option, + storage_gb: Option, + bandwidth_mbps: Option, + region: Option, + node_type: Option, + slice_formats: Option>, + rental_options: Option, + slice_prices: Option>, +} + +impl NodeCreationDataBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn location(mut self, location: impl Into) -> Self { + self.location = Some(location.into()); + self + } + + pub fn cpu_cores(mut self, cores: i32) -> Self { + self.cpu_cores = Some(cores); + self + } + + pub fn memory_gb(mut self, memory: i32) -> Self { + self.memory_gb = Some(memory); + self + } + + pub fn storage_gb(mut self, storage: i32) -> Self { + self.storage_gb = Some(storage); + self + } + + pub fn bandwidth_mbps(mut self, bandwidth: i32) -> Self { + self.bandwidth_mbps = Some(bandwidth); + self + } + + pub fn region(mut self, region: impl Into) -> Self { + self.region = Some(region.into()); + self + } + + pub fn node_type(mut self, node_type: impl Into) -> Self { + self.node_type = Some(node_type.into()); + self + } + + pub fn slice_formats(mut self, formats: Vec) -> Self { + self.slice_formats = Some(formats); + self + } + + pub fn rental_options(mut self, options: crate::models::user::NodeRentalOptions) -> Self { + self.rental_options = Some(options); + self + } + + pub fn slice_prices(mut self, prices: std::collections::HashMap) -> Self { + self.slice_prices = Some(prices); + self + } + + pub fn build(self) -> Result { + Ok(crate::services::farmer::NodeCreationData { + name: self.name.ok_or("name is required")?, + location: self.location.ok_or("location is required")?, + cpu_cores: self.cpu_cores.unwrap_or(4), + memory_gb: self.memory_gb.unwrap_or(8), + storage_gb: self.storage_gb.unwrap_or(100), + bandwidth_mbps: self.bandwidth_mbps.unwrap_or(100), + region: self.region, + node_type: self.node_type, + slice_formats: self.slice_formats, + rental_options: self.rental_options, + slice_prices: self.slice_prices, + slice_pricing: None, + }) + } +} + +// ============================================================================= +// FULL NODE PRICING BUILDER +// ============================================================================= + +#[derive(Default)] +pub struct FullNodePricingBuilder { + hourly: Option, + daily: Option, + monthly: Option, + yearly: Option, + auto_calculate: Option, + daily_discount_percent: Option, + monthly_discount_percent: Option, + yearly_discount_percent: Option, +} + +impl FullNodePricingBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn hourly(mut self, hourly: rust_decimal::Decimal) -> Self { + self.hourly = Some(hourly); + self + } + + pub fn daily(mut self, daily: rust_decimal::Decimal) -> Self { + self.daily = Some(daily); + self + } + + pub fn monthly(mut self, monthly: rust_decimal::Decimal) -> Self { + self.monthly = Some(monthly); + self + } + + pub fn yearly(mut self, yearly: rust_decimal::Decimal) -> Self { + self.yearly = Some(yearly); + self + } + + pub fn auto_calculate(mut self, auto_calculate: bool) -> Self { + self.auto_calculate = Some(auto_calculate); + self + } + + pub fn daily_discount_percent(mut self, percent: f32) -> Self { + self.daily_discount_percent = Some(percent); + self + } + + pub fn monthly_discount_percent(mut self, percent: f32) -> Self { + self.monthly_discount_percent = Some(percent); + self + } + + pub fn yearly_discount_percent(mut self, percent: f32) -> Self { + self.yearly_discount_percent = Some(percent); + self + } + + pub fn build(self) -> Result { + let mut pricing = crate::models::user::FullNodePricing { + hourly: self.hourly.unwrap_or_else(|| rust_decimal_macros::dec!(0)), + daily: self.daily.unwrap_or_else(|| rust_decimal_macros::dec!(0)), + monthly: self.monthly.unwrap_or_else(|| rust_decimal_macros::dec!(0)), + yearly: self.yearly.unwrap_or_else(|| rust_decimal_macros::dec!(0)), + auto_calculate: self.auto_calculate.unwrap_or(true), + daily_discount_percent: self.daily_discount_percent.unwrap_or(0.0), + monthly_discount_percent: self.monthly_discount_percent.unwrap_or(0.0), + yearly_discount_percent: self.yearly_discount_percent.unwrap_or(0.0), + }; + + // Auto-calculate rates if enabled and hourly rate is set + if pricing.auto_calculate && pricing.hourly > rust_decimal_macros::dec!(0) { + pricing.calculate_from_hourly(); + } + + Ok(pricing) + } +} + +// ============================================================================= +// NODE RENTAL OPTIONS BUILDER +// ============================================================================= + +#[derive(Default)] +pub struct NodeRentalOptionsBuilder { + slice_rental_enabled: Option, + full_node_rental_enabled: Option, + full_node_pricing: Option, + minimum_rental_days: Option, + maximum_rental_days: Option, + auto_renewal_enabled: Option, +} + +impl NodeRentalOptionsBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn slice_rental_enabled(mut self, enabled: bool) -> Self { + self.slice_rental_enabled = Some(enabled); + self + } + + pub fn full_node_rental_enabled(mut self, enabled: bool) -> Self { + self.full_node_rental_enabled = Some(enabled); + self + } + + pub fn full_node_pricing(mut self, pricing: crate::models::user::FullNodePricing) -> Self { + self.full_node_pricing = Some(pricing); + self + } + + pub fn minimum_rental_days(mut self, days: u32) -> Self { + self.minimum_rental_days = Some(days); + self + } + + pub fn maximum_rental_days(mut self, days: u32) -> Self { + self.maximum_rental_days = Some(days); + self + } + + pub fn auto_renewal_enabled(mut self, enabled: bool) -> Self { + self.auto_renewal_enabled = Some(enabled); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::NodeRentalOptions { + slice_rental_enabled: self.slice_rental_enabled.unwrap_or(true), + full_node_rental_enabled: self.full_node_rental_enabled.unwrap_or(false), + full_node_pricing: self.full_node_pricing, + minimum_rental_days: self.minimum_rental_days.unwrap_or(30), + maximum_rental_days: self.maximum_rental_days, + auto_renewal_enabled: self.auto_renewal_enabled.unwrap_or(false), + }) + } +} + +// ============================================================================= +// NODE STAKING OPTIONS BUILDER +// ============================================================================= + +#[derive(Default)] +pub struct NodeStakingOptionsBuilder { + staking_enabled: Option, + staked_amount: Option, + staking_start_date: Option>, + staking_period_months: Option, + early_withdrawal_allowed: Option, + early_withdrawal_penalty_percent: Option, +} + +impl NodeStakingOptionsBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn staking_enabled(mut self, enabled: bool) -> Self { + self.staking_enabled = Some(enabled); + self + } + + pub fn staked_amount(mut self, amount: Decimal) -> Self { + self.staked_amount = Some(amount); + self + } + + pub fn staking_start_date(mut self, date: DateTime) -> Self { + self.staking_start_date = Some(date); + self + } + + pub fn staking_period_months(mut self, months: u32) -> Self { + self.staking_period_months = Some(months); + self + } + + pub fn early_withdrawal_allowed(mut self, allowed: bool) -> Self { + self.early_withdrawal_allowed = Some(allowed); + self + } + + pub fn early_withdrawal_penalty_percent(mut self, percent: f32) -> Self { + self.early_withdrawal_penalty_percent = Some(percent); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::NodeStakingOptions { + staking_enabled: self.staking_enabled.unwrap_or(false), + staked_amount: self.staked_amount.unwrap_or(Decimal::ZERO), + staking_start_date: self.staking_start_date, + staking_period_months: self.staking_period_months.unwrap_or(12), + early_withdrawal_allowed: self.early_withdrawal_allowed.unwrap_or(true), + early_withdrawal_penalty_percent: self.early_withdrawal_penalty_percent.unwrap_or(25.0), + }) + } +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/notes.md b/heromodels/src/models/tfmarketplace/notes.md new file mode 100644 index 0000000..c70a351 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/notes.md @@ -0,0 +1,8 @@ +# Notes + +all id's of base objects are u32 +Cart is front end specific, +currency and exchange rates should be calculated by client +stuff such as decomal numbers related to presentation shouldnt be in base models +purchase doesnt need to now wether it is instant or cart +all base objects contain created_at and updated_at, so not needed to be added to every model \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/order.rs b/heromodels/src/models/tfmarketplace/order.rs new file mode 100644 index 0000000..fbfab0e --- /dev/null +++ b/heromodels/src/models/tfmarketplace/order.rs @@ -0,0 +1,402 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::CustomType; +use crate::models::tfmarketplace::user::ResourceUtilization; + +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +pub struct Order { + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + #[index] + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub items: Vec, + pub subtotal_base: Decimal, // In base currency + pub total_base: Decimal, // In base currency + pub base_currency: String, + pub currency_used: String, // Currency user paid in + pub currency_total: Decimal, // Amount in user's currency + pub conversion_rate: Decimal, // Rate used for conversion + pub status: OrderStatus, + pub payment_method: String, + pub payment_details: Option, + pub billing_address: Option
, + pub shipping_address: Option
, + pub notes: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct OrderItem { + pub product_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub product_name: String, + pub product_category: String, + pub quantity: u32, + pub unit_price_base: Decimal, // In base currency + pub total_price_base: Decimal, // In base currency + pub specifications: HashMap, + pub provider_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub provider_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum OrderStatus { + Pending, + Confirmed, + Processing, + Deployed, + Completed, + Cancelled, + Refunded, + Failed, +} + +/// Order summary for display purposes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderSummary { + pub subtotal: Decimal, + pub tax: Decimal, + pub shipping: Decimal, + pub discount: Decimal, + pub total: Decimal, + pub currency: String, + pub item_count: u32, +} + +impl Order { + pub fn new( + base_data: BaseModelData::new(), + // id: String - moved to base_data, + user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + base_currency: String, + currency_used: String, + conversion_rate: Decimal, + ) -> Self { + Self { + base_data: BaseModelData::new(), + user_id, + items: Vec::default(), + subtotal_base: Decimal::from(0), + total_base: Decimal::from(0), + base_currency, + currency_used, + currency_total: Decimal::from(0), + conversion_rate, + status: OrderStatus::Pending, + payment_method: String::new(), + payment_details: None, + billing_address: None, + shipping_address: None, + notes: None, + } + } + + pub fn add_item(&mut self, item: OrderItem) { + self.items.push(item); + self.calculate_totals(); + } + + pub fn calculate_totals(&mut self) { + self.subtotal_base = self.items.iter() + .map(|item| item.total_price_base) + .sum(); + self.total_base = self.subtotal_base; // Add taxes, fees, etc. here + self.currency_total = self.total_base * self.conversion_rate; + self.base_data.modified_at = Utc::now().timestamp(); + } + + pub fn update_status(&mut self, status: OrderStatus) { + self.status = status; + self.base_data.modified_at = Utc::now().timestamp(); + } + + pub fn set_payment_details(&mut self, payment_details: PaymentDetails) { + self.payment_details = Some(payment_details); + self.base_data.modified_at = Utc::now().timestamp(); + } + + pub fn get_item_count(&self) -> u32 { + self.items.iter().map(|item| item.quantity).sum() + } +} + +impl OrderItem { + pub fn new( + product_base_data: BaseModelData::new(), + // id: String - moved to base_data, + product_name: String, + product_category: String, + quantity: u32, + unit_price_base: Decimal, + provider_base_data: BaseModelData::new(), + // id: String - moved to base_data, + provider_name: String, + ) -> Self { + Self { + product_id, + product_name, + product_category, + quantity, + unit_price_base, + total_price_base: unit_price_base * Decimal::from(quantity), + specifications: HashMap::default(), + provider_id, + provider_name, + } + } + + pub fn add_specification(&mut self, key: String, value: serde_json::Value) { + self.specifications.insert(key, value); + } + + pub fn update_quantity(&mut self, quantity: u32) { + self.quantity = quantity; + self.total_price_base = self.unit_price_base * Decimal::from(quantity); + } +} + + +#[derive(Default)] +pub struct OrderBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + user_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + items: Vec, + subtotal_base: Option, + total_base: Option, + base_currency: Option, + currency_used: Option, + currency_total: Option, + conversion_rate: Option, + status: Option, + payment_method: Option, + payment_details: Option, + billing_address: Option
, + shipping_address: Option
, + notes: Option, + purchase_type: Option, + // created_at: Option> - moved to base_data, + // updated_at: Option> - moved to base_data, +} + +impl OrderBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self) -> Self{ + self.base_data.id = Some(id.into()); + self + } + + pub fn user_id(mut self, user_id: &str, name: &str) -> Self{ + self.user_id = Some(user_id.into()); + self + } + + pub fn add_item(mut self, item: OrderItem) -> Self { + self.items.push(item); + self + } + + pub fn items(mut self, items: Vec) -> Self { + self.items = items; + self + } + + pub fn subtotal_base(mut self, subtotal: Decimal) -> Self { + self.subtotal_base = Some(subtotal); + self + } + + pub fn total_base(mut self, total: Decimal) -> Self { + self.total_base = Some(total); + self + } + + pub fn base_currency(mut self, currency: impl Into) -> Self { + self.base_currency = Some(currency.into()); + self + } + + pub fn currency_used(mut self, currency: impl Into) -> Self { + self.currency_used = Some(currency.into()); + self + } + + pub fn currency_total(mut self, total: Decimal) -> Self { + self.currency_total = Some(total); + self + } + + pub fn conversion_rate(mut self, rate: Decimal) -> Self { + self.conversion_rate = Some(rate); + self + } + + pub fn status(mut self, status: OrderStatus) -> Self { + self.status = Some(status); + self + } + + pub fn payment_method(mut self, method: impl Into) -> Self { + self.payment_method = Some(method.into()); + self + } + + pub fn payment_details(mut self, details: PaymentDetails) -> Self { + self.payment_details = Some(details); + self + } + + pub fn billing_address(mut self, address: Address) -> Self { + self.billing_address = Some(address); + self + } + + pub fn shipping_address(mut self, address: Address) -> Self { + self.shipping_address = Some(address); + self + } + + pub fn notes(mut self, notes: impl Into) -> Self { + self.notes = Some(notes.into()); + self + } + + pub fn purchase_type(mut self, purchase_type: PurchaseType) -> Self { + self.purchase_type = Some(purchase_type); + self + } + + pub fn build(self) -> Result { + let now = Utc::now(); + let subtotal = self.subtotal_base.unwrap_or_else(|| { + self.items.iter().map(|item| item.total_price_base).sum() + }); + + Ok(Order { + base_data: BaseModelData::new(), + // id: self.base_data.id.ok_or("id is required")? - moved to base_data, + user_base_data: BaseModelData::new(), + // id: self.user_id.ok_or("user_id is required")? - moved to base_data, + items: self.items, + subtotal_base: subtotal, + total_base: self.total_base.unwrap_or(subtotal), + base_currency: self.base_currency.unwrap_or_else(|| "USD".to_string()), + currency_used: self.currency_used.unwrap_or_else(|| "USD".to_string()), + currency_total: self.currency_total.unwrap_or(subtotal), + conversion_rate: self.conversion_rate.unwrap_or_else(|| Decimal::from(1)), + status: self.status.unwrap_or(OrderStatus::Pending), + payment_method: self.payment_method.unwrap_or_else(|| "credit_card".to_string()), + payment_details: self.payment_details, + billing_address: self.billing_address, + shipping_address: self.shipping_address, + notes: self.notes, + purchase_type: self.purchase_type.unwrap_or(PurchaseType::Cart), + // created_at: self.base_data.created_at.unwrap_or(now) - moved to base_data, + // updated_at: self.base_data.updated_at.unwrap_or(now) - moved to base_data, + }) + } +} + +impl Order { + pub fn builder() -> OrderBuilder { + OrderBuilder::new() + } +} + +#[derive(Default)] +pub struct OrderItemBuilder { + product_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + product_name: Option, + product_category: Option, + quantity: Option, + unit_price_base: Option, + total_price_base: Option, + specifications: HashMap, + provider_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + provider_name: Option, +} + +impl OrderItemBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn product_id(mut self) -> Self{ + self.product_id = Some(id.into()); + self + } + + pub fn product_name(mut self, name: impl Into) -> Self { + self.product_name = Some(name.into()); + self + } + + pub fn product_category(mut self, category: impl Into) -> Self { + self.product_category = Some(category.into()); + self + } + + pub fn quantity(mut self, quantity: u32) -> Self { + self.quantity = Some(quantity); + self + } + + pub fn unit_price_base(mut self, price: Decimal) -> Self { + self.unit_price_base = Some(price); + self + } + + pub fn add_specification(mut self, key: impl Into, value: Value) -> Self { + self.specifications.insert(key.into(), value); + self + } + + pub fn provider_id(mut self) -> Self{ + self.provider_id = Some(id.into()); + self + } + + pub fn provider_name(mut self, name: impl Into) -> Self { + self.provider_name = Some(name.into()); + self + } + + pub fn build(self) -> Result { + let quantity = self.quantity.unwrap_or(1); + let unit_price = self.unit_price_base.ok_or("unit_price_base is required")?; + let total_price = self.total_price_base.unwrap_or(unit_price * Decimal::from(quantity)); + + Ok(OrderItem { + product_base_data: BaseModelData::new(), + // id: self.product_id.ok_or("product_id is required")? - moved to base_data, + product_name: self.product_name.ok_or("product_name is required")?, + product_category: self.product_category.ok_or("product_category is required")?, + quantity, + unit_price_base: unit_price, + total_price_base: total_price, + specifications: self.specifications, + provider_base_data: BaseModelData::new(), + // id: self.provider_id.ok_or("provider_id is required")? - moved to base_data, + provider_name: self.provider_name.ok_or("provider_name is required")?, + }) + } +} + +impl OrderItem { + pub fn builder() -> OrderItemBuilder { + OrderItemBuilder::new() + } +} diff --git a/heromodels/src/models/tfmarketplace/payment.rs b/heromodels/src/models/tfmarketplace/payment.rs new file mode 100644 index 0000000..0c8a80f --- /dev/null +++ b/heromodels/src/models/tfmarketplace/payment.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use crate::models::tfmarketplace::user::ResourceUtilization; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentDetails { + pub payment_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub payment_method: PaymentMethod, + pub transaction_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + pub payment_status: PaymentStatus, + pub payment_timestamp: Option>, + pub failure_reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentMethod { + CreditCard { + last_four: String, + card_type: String, + }, + BankTransfer { + bank_name: String, + account_last_four: String, + }, + Cryptocurrency { + currency: String, + wallet_address: String, + }, + Token { + token_type: String, + wallet_address: String, + }, + Mock { + method_name: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentStatus { + Pending, + Processing, + Completed, + Failed, + Cancelled, + Refunded, +} + +impl PaymentDetails { + pub fn new(payment_id: &str, name: &str) -> Self { + Self { + payment_id, + payment_method, + transaction_base_data: BaseModelData::new(), + // id: None - moved to base_data, + payment_status: PaymentStatus::Pending, + payment_timestamp: None, + failure_reason: None, + } + } + + pub fn mark_completed(&mut self, transaction_id: String) { - moved to base_data + self.transaction_id = Some(transaction_id); + self.payment_status = PaymentStatus::Completed; + self.payment_timestamp = Some(Utc::now()); + } + + pub fn mark_failed(&mut self, reason: String) { + self.payment_status = PaymentStatus::Failed; + self.failure_reason = Some(reason); + self.payment_timestamp = Some(Utc::now()); + } +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/pool.rs b/heromodels/src/models/tfmarketplace/pool.rs new file mode 100644 index 0000000..2decc62 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/pool.rs @@ -0,0 +1,105 @@ +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use crate::models::tfmarketplace::user::ResourceUtilization; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LiquidityPool { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub name: String, + pub token_a: String, + pub token_b: String, + pub reserve_a: Decimal, + pub reserve_b: Decimal, + pub exchange_rate: Decimal, + pub liquidity: Decimal, + pub volume_24h: Decimal, + pub fee_percentage: Decimal, + pub status: PoolStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PoolStatus { + Active, + Paused, + Maintenance, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeRequest { + pub pool_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub from_token: String, + pub to_token: String, + pub amount: Decimal, + pub min_receive: Option, + pub slippage_tolerance: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeResponse { + pub success: bool, + pub message: String, + pub transaction_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + pub from_amount: Option, + pub to_amount: Option, + pub exchange_rate: Option, + pub fee: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StakeRequest { + pub amount: Decimal, + pub duration_months: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StakePosition { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub amount: Decimal, + pub start_date: DateTime, + pub end_date: DateTime, + pub discount_percentage: Decimal, + pub reputation_bonus: i32, + pub status: StakeStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum StakeStatus { + Active, + Completed, + Withdrawn, +} + +/// Pool analytics data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PoolAnalytics { + pub price_history: Vec, + pub volume_history: Vec, + pub liquidity_distribution: HashMap, + pub staking_distribution: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PricePoint { + pub timestamp: DateTime, + pub price: Decimal, + pub volume: Decimal, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VolumePoint { + pub date: String, + pub volume: Decimal, +} \ No newline at end of file diff --git a/heromodels/src/models/tfmarketplace/product.rs b/heromodels/src/models/tfmarketplace/product.rs new file mode 100644 index 0000000..b3632d0 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/product.rs @@ -0,0 +1,660 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::CustomType; + +/// Generic product structure that can represent any marketplace item +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +pub struct Product { + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + #[index] + pub name: String, + pub category: ProductCategory, + pub description: String, + pub price: Price, + pub attributes: HashMap, // Generic attributes + pub provider_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub provider_name: String, + pub availability: ProductAvailability, + pub metadata: ProductMetadata, // Extensible metadata +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Price { + pub base_amount: Decimal, + pub currency: u32, +} + +/// Configurable product categories +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProductCategory { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub name: String, + pub display_name: String, + pub description: String, + pub attribute_schema: Vec, // Defines allowed attributes + pub parent_category: Option, + pub is_active: bool, +} + +/// Generic attribute system for any product type +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProductAttribute { + pub key: String, + pub value: serde_json::Value, + pub attribute_type: AttributeType, + pub is_searchable: bool, + pub is_filterable: bool, + pub display_order: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum AttributeType { + Text, + Number, + SliceConfiguration, + Boolean, + Select(Vec), // Predefined options + MultiSelect(Vec), + Range { min: f64, max: f64 }, + Custom(String), // For marketplace-specific types +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct AttributeDefinition { + pub key: String, + pub name: String, + pub attribute_type: AttributeType, + pub is_required: bool, + pub is_searchable: bool, + pub is_filterable: bool, + pub validation_rules: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ValidationRule { + MinLength(usize), + MaxLength(usize), + MinValue(f64), + MaxValue(f64), + Pattern(String), + Custom(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ProductAvailability { + Available, + Limited, + Unavailable, + PreOrder, + Custom(String), // For marketplace-specific availability states +} + +impl Default for ProductAvailability { + fn default() -> Self { + ProductAvailability::Available + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ProductVisibility { + Public, + Private, + Draft, + Archived, +} + +impl Default for ProductVisibility { + fn default() -> Self { + ProductVisibility::Public + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct ProductMetadata { + pub tags: Vec, + pub location: Option, + pub rating: Option, + pub review_count: u32, + pub featured: bool, + pub last_updated: chrono::DateTime, + pub visibility: ProductVisibility, + pub seo_keywords: Vec, + pub custom_fields: HashMap, +} + +/// Support for different pricing models +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PricingModel { + OneTime, // Single purchase + Recurring { interval: String }, // Subscription + UsageBased { unit: String }, // Pay per use + Tiered(Vec), // Volume discounts + Custom(String), // Marketplace-specific +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PriceTier { + pub min_quantity: u32, + pub max_quantity: Option, + pub price_per_unit: Decimal, + pub discount_percentage: Option, +} + +impl Product { + pub fn new( + name: String, + category: ProductCategory, + description: String, + price: Price, + provider_base_data: BaseModelData::new(), + // id: String - moved to base_data, + provider_name: String, + ) -> Self { + Self { + base_data: BaseModelData::new(), + name, + category, + description, + price, + attributes: HashMap::default(), + provider_id, + provider_name, + availability: ProductAvailability::Available, + metadata: ProductMetadata { + tags: Vec::default(), + location: None, + rating: None, + review_count: 0, + featured: false, + last_updated: chrono::Utc::now(), + visibility: ProductVisibility::Public, + seo_keywords: Vec::new(), + custom_fields: HashMap::default(), + }, + } + } + + pub fn add_attribute(&mut self, key: String, value: serde_json::Value, attribute_type: AttributeType) { + let attribute = ProductAttribute { + key: key.clone(), + value, + attribute_type, + is_searchable: true, + is_filterable: true, + display_order: None, + }; + self.attributes.insert(key, attribute); + self.base_data.modified_at = Utc::now().timestamp(); + } + + pub fn set_featured(&mut self, featured: bool) { + self.metadata.featured = featured; + self.base_data.modified_at = Utc::now().timestamp(); + } + + pub fn add_tag(&mut self, tag: String) { + if !self.metadata.tags.contains(&tag) { + self.metadata.tags.push(tag); + self.base_data.modified_at = Utc::now().timestamp(); + } + } + + pub fn set_rating(&mut self, rating: f32, review_count: u32) { + self.metadata.rating = Some(rating); + self.metadata.review_count = review_count; + self.base_data.modified_at = Utc::now().timestamp(); + } +} + +impl ProductCategory { + pub fn new() -> Self { + // id: String - moved to base_data, name: String, display_name: String, description: String) -> Self { + Self { + base_data: BaseModelData::new(), + name, + display_name, + description, + attribute_schema: Vec::default(), + parent_category: None, + is_active: true, + } + } + + /// Add attribute definition to category schema + pub fn add_attribute_definition(&mut self, definition: AttributeDefinition) { + self.attribute_schema.push(definition); + } +} + +impl Product { + /// Create a slice product from farmer configuration + pub fn create_slice_product( + base_data: BaseModelData::new(), + // id: String - moved to base_data, + farmer_name: String, + slice_name: String, + slice_config: SliceConfiguration, + price_per_hour: Decimal, + ) -> Self { + let category = ProductCategory { + base_data: BaseModelData::new(), + // id: "compute_slices".to_string() - moved to base_data, + name: "Compute Slices".to_string(), + display_name: "Compute Slices".to_string(), + description: "Virtual compute resources".to_string(), + attribute_schema: Vec::new(), + parent_category: None, + is_active: true, + }; + let price = Price { + base_amount: price_per_hour, + currency: 1, // USD currency ID + }; + let mut product = Self::new( + base_data, + slice_name, + category, + format!("Compute slice with {} vCPU, {}GB RAM, {}GB storage", + slice_config.cpu_cores, slice_config.memory_gb, slice_config.storage_gb), + price, + farmer_id, + farmer_name, + ); + + // Add slice-specific attributes + product.add_attribute( + "cpu_cores".to_string(), + serde_json::Value::Number(serde_json::Number::from(slice_config.cpu_cores)), + AttributeType::Number, + ); + + product.add_attribute( + "memory_gb".to_string(), + serde_json::Value::Number(serde_json::Number::from(slice_config.memory_gb)), + AttributeType::Number, + ); + + product.add_attribute( + "storage_gb".to_string(), + serde_json::Value::Number(serde_json::Number::from(slice_config.storage_gb)), + AttributeType::Number, + ); + + product.add_attribute( + "bandwidth_mbps".to_string(), + serde_json::Value::Number(serde_json::Number::from(slice_config.bandwidth_mbps)), + AttributeType::Number, + ); + + product.add_attribute( + "min_uptime_sla".to_string(), + serde_json::Value::Number(serde_json::Number::from_f64(slice_config.min_uptime_sla as f64).unwrap()), + AttributeType::Number, + ); + + product.add_attribute( + "public_ips".to_string(), + serde_json::Value::Number(serde_json::Number::from(slice_config.public_ips)), + AttributeType::Number, + ); + + if let Some(ref node_id) = slice_config.node_id { + product.add_attribute( + "node_id".to_string(), + serde_json::Value::String(node_id.clone()), + AttributeType::Text, + ); + } + + product.add_attribute( + "slice_type".to_string(), + serde_json::Value::String(format!("{:?}", slice_config.slice_type)), + AttributeType::Text, + ); + + // Add slice configuration as a complex attribute + product.add_attribute( + "slice_configuration".to_string(), + serde_json::to_value(&slice_config).unwrap(), + AttributeType::SliceConfiguration, + ); + + // Add relevant tags + product.add_tag("compute".to_string()); + product.add_tag("slice".to_string()); + product.add_tag(format!("{:?}", slice_config.slice_type).to_lowercase()); + + product + } + + /// Check if this product is a slice + pub fn is_slice(&self) -> bool { + self.category.id == "compute_slices" || + self.attributes.contains_key("slice_configuration") + } + + /// Get slice configuration from product attributes + pub fn get_slice_configuration(&self) -> Option { + self.attributes.get("slice_configuration") + .and_then(|attr| serde_json::from_value(attr.value.clone()).ok()) + } + + /// Update slice configuration + pub fn update_slice_configuration(&mut self, config: SliceConfiguration) { + if self.is_slice() { + self.add_attribute( + "slice_configuration".to_string(), + serde_json::to_value(&config).unwrap(), + AttributeType::SliceConfiguration, + ); + + // Update individual attributes for searchability + self.add_attribute( + "cpu_cores".to_string(), + serde_json::Value::Number(serde_json::Number::from(config.cpu_cores)), + AttributeType::Number, + ); + + self.add_attribute( + "memory_gb".to_string(), + serde_json::Value::Number(serde_json::Number::from(config.memory_gb)), + AttributeType::Number, + ); + + self.add_attribute( + "storage_gb".to_string(), + serde_json::Value::Number(serde_json::Number::from(config.storage_gb)), + AttributeType::Number, + ); + } + } + + /// Check if slice fits within node capacity + pub fn slice_fits_in_node(&self, node_capacity: &crate::models::user::NodeCapacity) -> bool { + if let Some(config) = self.get_slice_configuration() { + config.cpu_cores <= node_capacity.cpu_cores && + config.memory_gb <= node_capacity.memory_gb && + config.storage_gb <= node_capacity.storage_gb && + config.bandwidth_mbps <= node_capacity.bandwidth_mbps + } else { + false + } + } + /// Create a full node product from a FarmNode + pub fn create_full_node_product( + node: &crate::models::user::FarmNode, + farmer_email: &str, + farmer_name: &str, + ) -> Self { + let category = ProductCategory { + base_data: BaseModelData::new(), + // id: "3nodes".to_string() - moved to base_data, + name: "3Nodes".to_string(), + display_name: "3Nodes".to_string(), + description: "Full node rentals".to_string(), + attribute_schema: Vec::new(), + parent_category: None, + is_active: true, + }; + let price = Price { + base_amount: node.rental_options + .as_ref() + .and_then(|opts| opts.full_node_pricing.as_ref()) + .map(|pricing| pricing.monthly) + .unwrap_or_else(|| Decimal::from(200)), // Default price + currency: 1, // USD currency ID + }; + let mut product = Product { + base_data: BaseModelData::new(), + name: format!("Full Node: {}", node.name), + category, + description: format!( + "Exclusive access to {} with {} CPU cores, {}GB RAM, {}GB storage in {}", + node.name, node.capacity.cpu_cores, node.capacity.memory_gb, + node.capacity.storage_gb, node.location + ), + price, + attributes: HashMap::new(), + provider_base_data: BaseModelData::new(), + // id: farmer_email.to_string() - moved to base_data, + provider_name: farmer_name.to_string(), + availability: match node.availability_status { + crate::models::user::NodeAvailabilityStatus::Available => ProductAvailability::Available, + crate::models::user::NodeAvailabilityStatus::PartiallyRented => ProductAvailability::Limited, + _ => ProductAvailability::Unavailable, + }, + metadata: ProductMetadata { + tags: vec!["full-node".to_string(), "exclusive".to_string(), node.region.clone()], + location: Some(node.location.clone()), + rating: None, + review_count: 0, + featured: false, + last_updated: chrono::Utc::now(), + visibility: ProductVisibility::Public, + seo_keywords: Vec::new(), + custom_fields: HashMap::new(), + }, + }; + + // Add node-specific attributes + product.add_attribute( + "node_id".to_string(), + serde_json::Value::String(node.id.clone()), + AttributeType::Text, + ); + + product.add_attribute( + "rental_type".to_string(), + serde_json::Value::String("full_node".to_string()), + AttributeType::Text, + ); + + product.add_attribute( + "cpu_cores".to_string(), + serde_json::Value::Number(serde_json::Number::from(node.capacity.cpu_cores)), + AttributeType::Number, + ); + + product.add_attribute( + "memory_gb".to_string(), + serde_json::Value::Number(serde_json::Number::from(node.capacity.memory_gb)), + AttributeType::Number, + ); + + product.add_attribute( + "storage_gb".to_string(), + serde_json::Value::Number(serde_json::Number::from(node.capacity.storage_gb)), + AttributeType::Number, + ); + + product.add_attribute( + "bandwidth_mbps".to_string(), + serde_json::Value::Number(serde_json::Number::from(node.capacity.bandwidth_mbps)), + AttributeType::Number, + ); + + product.add_attribute( + "location".to_string(), + serde_json::Value::String(node.location.clone()), + AttributeType::Text, + ); + + product.add_attribute( + "uptime_percentage".to_string(), + serde_json::Value::Number(serde_json::Number::from_f64(node.uptime_percentage as f64).unwrap_or_else(|| serde_json::Number::from(0))), + AttributeType::Number, + ); + + product.add_attribute( + "health_score".to_string(), + serde_json::Value::Number(serde_json::Number::from_f64(node.health_score as f64).unwrap_or_else(|| serde_json::Number::from(0))), + AttributeType::Number, + ); + + product + } + + /// Check if this product represents a full node + pub fn is_full_node(&self) -> bool { + self.attributes.get("rental_type") + .and_then(|attr| attr.value.as_str()) + .map(|s| s == "full_node") + .unwrap_or(false) + } + + /// Get the node ID if this is a node product + pub fn get_node_id(&self) -> Option { + self.attributes.get("node_id") + .and_then(|attr| attr.value.as_str()) + .map(|s| s.to_string()) + } +} + +impl ProductCategory { + pub fn set_parent_category(&mut self, parent_id: String) { + self.parent_category = Some(parent_id); + } +} + +impl AttributeDefinition { + pub fn new( + key: String, + name: String, + attribute_type: AttributeType, + is_required: bool, + ) -> Self { + Self { + key, + name, + attribute_type, + is_required, + is_searchable: true, + is_filterable: true, + validation_rules: Vec::default(), + } + } + + pub fn add_validation_rule(&mut self, rule: ValidationRule) { + self.validation_rules.push(rule); + } +} + + +#[derive(Default)] +pub struct ProductBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + name: Option, + category_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + description: Option, + base_price: Option, + base_currency: Option, + attributes: HashMap, + provider_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + provider_name: Option, + availability: Option, + metadata: Option, + // created_at: Option> - moved to base_data, + // updated_at: Option> - moved to base_data, +} + +impl ProductBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self, id: impl Into) -> Self { + self.base_data.id = Some(id.into()); + self + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn category_id(mut self, category_id: impl Into) -> Self { + self.category_id = Some(category_id.into()); + self + } + + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + + pub fn base_price(mut self, price: Decimal) -> Self { + self.base_price = Some(price); + self + } + + pub fn base_currency(mut self, currency: impl Into) -> Self { + self.base_currency = Some(currency.into()); + self + } + + pub fn add_attribute(mut self, key: impl Into, attribute: ProductAttribute) -> Self { + self.attributes.insert(key.into(), attribute); + self + } + + pub fn provider_id(mut self, provider_id: impl Into) -> Self { + self.provider_id = Some(provider_id.into()); + self + } + + pub fn provider_name(mut self, provider_name: impl Into) -> Self { + self.provider_name = Some(provider_name.into()); + self + } + + pub fn availability(mut self, availability: ProductAvailability) -> Self { + self.availability = Some(availability); + self + } + + pub fn metadata(mut self, metadata: ProductMetadata) -> Self { + self.metadata = Some(metadata); + self + } + + pub fn build(self) -> Result { + let now = Utc::now(); + Ok(Product { + base_data: BaseModelData::new(), + // id: self.base_data.id.ok_or("id is required")? - moved to base_data, + name: self.name.ok_or("name is required")?, + category_base_data: BaseModelData::new(), + // id: self.category_id.ok_or("category_id is required")? - moved to base_data, + description: self.description.unwrap_or_default(), + base_price: self.base_price.ok_or("base_price is required")?, + base_currency: self.base_currency.unwrap_or_else(|| "USD".to_string()), + attributes: self.attributes, + provider_base_data: BaseModelData::new(), + // id: self.provider_id.ok_or("provider_id is required")? - moved to base_data, + provider_name: self.provider_name.ok_or("provider_name is required")?, + availability: self.availability.unwrap_or_default(), + metadata: self.metadata.unwrap_or_default(), + // created_at: self.base_data.created_at.unwrap_or(now) - moved to base_data, + // updated_at: self.base_data.updated_at.unwrap_or(now) - moved to base_data, + }) + } +} + +impl Product { + pub fn builder() -> ProductBuilder { + ProductBuilder::new() + } +} diff --git a/heromodels/src/models/tfmarketplace/service.rs b/heromodels/src/models/tfmarketplace/service.rs new file mode 100644 index 0000000..29cecdc --- /dev/null +++ b/heromodels/src/models/tfmarketplace/service.rs @@ -0,0 +1,297 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize, Deserializer}; +use rust_decimal::Decimal; +use std::str::FromStr; +use heromodels_core::BaseModelData; +use crate::models::tfmarketplace::user::ResourceUtilization; + +/// Service Provider-specific data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceProviderData { + pub active_services: i32, + pub total_clients: i32, + pub monthly_revenue_usd: i32, + pub total_revenue_usd: i32, + pub service_rating: f32, + pub services: Vec, + pub client_requests: Vec, + pub revenue_history: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Service { + pub base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub name: String, + pub category: String, + pub description: String, + pub price_per_hour_usd: i32, + pub status: String, + pub clients: i32, + pub rating: f32, + pub total_hours: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceRequest { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub client_name: String, + pub service_name: String, + pub status: String, + pub requested_date: String, + pub estimated_hours: i32, + pub budget: i32, + pub priority: String, + #[serde(default)] + pub progress: Option, + #[serde(default)] + pub completed_date: Option, + #[serde(default)] + pub client_email: Option, + #[serde(default)] + pub client_phone: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub created_date: Option, +} + +/// Service booking record for customers who purchase services +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceBooking { + pub base_data: BaseModelData::new(), + // id: String - moved to base_data, // Same as ServiceRequest.id for cross-reference + pub service_base_data: BaseModelData::new(), + // id: String - moved to base_data, // Reference to original service + pub service_name: String, + pub provider_email: String, // Who provides the service + pub customer_email: String, // Who booked the service + pub budget: i32, + pub estimated_hours: i32, + pub status: String, // "Pending", "In Progress", "Completed" + pub requested_date: String, + pub priority: String, + pub description: Option, + pub booking_date: String, // When customer booked + pub client_phone: Option, + pub progress: Option, + pub completed_date: Option, +} + +/// Customer Service-specific data (for users who book services) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CustomerServiceData { + pub active_bookings: i32, + pub completed_bookings: i32, + pub total_spent: i32, + pub monthly_spending: i32, + pub average_rating_given: f32, + pub service_bookings: Vec, + pub favorite_providers: Vec, + pub spending_history: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SpendingRecord { + pub date: String, + pub amount: i32, + pub service_name: String, + pub provider_name: String, +} + + +#[derive(Default)] +pub struct ServiceBookingBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + service_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + service_name: Option, + provider_email: Option, + customer_email: Option, + budget: Option, + estimated_hours: Option, + status: Option, + requested_date: Option, + priority: Option, + description: Option, + booking_date: Option, +} + +impl ServiceBookingBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self) -> Self{ + self.base_data.id = Some(id.to_string()); + self + } + + pub fn service_id(mut self, service_id: &str, name: &str) -> Self{ + self.service_id = Some(service_id.to_string()); + self + } + + pub fn service_name(mut self, service_name: &str) -> Self { + self.service_name = Some(service_name.to_string()); + self + } + + pub fn provider_email(mut self, provider_email: &str) -> Self { + self.provider_email = Some(provider_email.to_string()); + self + } + + pub fn customer_email(mut self, customer_email: &str) -> Self { + self.customer_email = Some(customer_email.to_string()); + self + } + + pub fn budget(mut self, budget: i32) -> Self { + self.budget = Some(budget); + self + } + + pub fn estimated_hours(mut self, hours: i32) -> Self { + self.estimated_hours = Some(hours); + self + } + + pub fn status(mut self, status: &str) -> Self { + self.status = Some(status.to_string()); + self + } + + pub fn requested_date(mut self, date: &str) -> Self { + self.requested_date = Some(date.to_string()); + self + } + + pub fn priority(mut self, priority: &str) -> Self { + self.priority = Some(priority.to_string()); + self + } + + pub fn description(mut self, description: Option) -> Self { + self.description = description; + self + } + + pub fn booking_date(mut self, date: &str) -> Self { + self.booking_date = Some(date.to_string()); + self + } + + pub fn build(self) -> Result { + Ok(ServiceBooking { + base_data: BaseModelData::new(), + // id: self.base_data.id.ok_or("ID is required")? - moved to base_data, + service_base_data: BaseModelData::new(), + // id: self.service_id.ok_or("Service ID is required")? - moved to base_data, + service_name: self.service_name.ok_or("Service name is required")?, + provider_email: self.provider_email.ok_or("Provider email is required")?, + customer_email: self.customer_email.ok_or("Customer email is required")?, + budget: self.budget.unwrap_or(0), + estimated_hours: self.estimated_hours.unwrap_or(0), + status: self.status.unwrap_or_else(|| "Pending".to_string()), + requested_date: self.requested_date.unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%d").to_string()), + priority: self.priority.unwrap_or_else(|| "Medium".to_string()), + description: self.description, + booking_date: self.booking_date.unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%d").to_string()), + client_phone: None, + progress: None, + completed_date: None, + }) + } +} + +impl ServiceBooking { + pub fn builder() -> ServiceBookingBuilder { + ServiceBookingBuilder::new() + } +} + +// ============================================================================= +// CUSTOMER SERVICE DATA BUILDER +// ============================================================================= + +#[derive(Default)] +pub struct CustomerServiceDataBuilder { + active_bookings: Option, + completed_bookings: Option, + total_spent: Option, + monthly_spending: Option, + average_rating_given: Option, + service_bookings: Option>, + favorite_providers: Option>, + spending_history: Option>, +} + +impl CustomerServiceDataBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn active_bookings(mut self, count: i32) -> Self { + self.active_bookings = Some(count); + self + } + + pub fn completed_bookings(mut self, count: i32) -> Self { + self.completed_bookings = Some(count); + self + } + + pub fn total_spent(mut self, amount: i32) -> Self { + self.total_spent = Some(amount); + self + } + + pub fn monthly_spending(mut self, amount: i32) -> Self { + self.monthly_spending = Some(amount); + self + } + + pub fn average_rating_given(mut self, rating: f32) -> Self { + self.average_rating_given = Some(rating); + self + } + + pub fn service_bookings(mut self, bookings: Vec) -> Self { + self.service_bookings = Some(bookings); + self + } + + pub fn favorite_providers(mut self, providers: Vec) -> Self { + self.favorite_providers = Some(providers); + self + } + + pub fn spending_history(mut self, history: Vec) -> Self { + self.spending_history = Some(history); + self + } + + pub fn build(self) -> Result { + Ok(crate::models::user::CustomerServiceData { + active_bookings: self.active_bookings.unwrap_or(0), + completed_bookings: self.completed_bookings.unwrap_or(0), + total_spent: self.total_spent.unwrap_or(0), + monthly_spending: self.monthly_spending.unwrap_or(0), + average_rating_given: self.average_rating_given.unwrap_or(0.0), + service_bookings: self.service_bookings.unwrap_or_default(), + favorite_providers: self.favorite_providers.unwrap_or_default(), + spending_history: self.spending_history.unwrap_or_default(), + }) + } +} + +impl crate::models::user::CustomerServiceData { + pub fn builder() -> CustomerServiceDataBuilder { + CustomerServiceDataBuilder::new() + } +} diff --git a/heromodels/src/models/tfmarketplace/slice.rs b/heromodels/src/models/tfmarketplace/slice.rs new file mode 100644 index 0000000..cdcc21b --- /dev/null +++ b/heromodels/src/models/tfmarketplace/slice.rs @@ -0,0 +1,200 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use heromodels_core::BaseModelData; +use crate::models::tfmarketplace::user::ResourceUtilization; + +/// Slice configuration data structure for product attributes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SliceConfiguration { + pub cpu_cores: i32, + pub memory_gb: i32, + pub storage_gb: i32, + pub bandwidth_mbps: i32, + pub min_uptime_sla: f32, + pub public_ips: i32, + pub node_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + pub slice_type: SliceType, + #[serde(default)] + pub pricing: SlicePricing, +} + +/// Enhanced pricing structure for slices with multiple time periods +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlicePricing { + pub hourly: Decimal, + pub daily: Decimal, + pub monthly: Decimal, + pub yearly: Decimal, +} + +impl Default for SlicePricing { + fn default() -> Self { + Self { + hourly: Decimal::ZERO, + daily: Decimal::ZERO, + monthly: Decimal::ZERO, + yearly: Decimal::ZERO, + } + } +} + +impl SlicePricing { + /// Create pricing from hourly rate with automatic calculation + pub fn from_hourly(hourly_rate: Decimal, daily_discount: f32, monthly_discount: f32, yearly_discount: f32) -> Self { + let base_daily = hourly_rate * Decimal::from(24); + let base_monthly = hourly_rate * Decimal::from(24 * 30); + let base_yearly = hourly_rate * Decimal::from(24 * 365); + + Self { + hourly: hourly_rate, + daily: base_daily * Decimal::try_from(1.0 - daily_discount / 100.0).unwrap_or(Decimal::ONE), + monthly: base_monthly * Decimal::try_from(1.0 - monthly_discount / 100.0).unwrap_or(Decimal::ONE), + yearly: base_yearly * Decimal::try_from(1.0 - yearly_discount / 100.0).unwrap_or(Decimal::ONE), + } + } + + /// Calculate savings compared to hourly rate + pub fn calculate_savings(&self) -> (Decimal, Decimal, Decimal) { + let hourly_equivalent_daily = self.hourly * Decimal::from(24); + let hourly_equivalent_monthly = self.hourly * Decimal::from(24 * 30); + let hourly_equivalent_yearly = self.hourly * Decimal::from(24 * 365); + + let daily_savings = hourly_equivalent_daily - self.daily; + let monthly_savings = hourly_equivalent_monthly - self.monthly; + let yearly_savings = hourly_equivalent_yearly - self.yearly; + + (daily_savings, monthly_savings, yearly_savings) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SliceType { + Basic, + Standard, + Premium, + Custom, +} + +#[derive(Default)] +pub struct SliceProductBuilder { + farmer_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + farmer_name: Option, + slice_name: Option, + cpu_cores: Option, + memory_gb: Option, + storage_gb: Option, + bandwidth_mbps: Option, + min_uptime_sla: Option, + public_ips: Option, + node_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + slice_type: Option, + price_per_hour: Option, +} + +impl SliceProductBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn farmer_id(mut self, farmer_id: &str, name: &str) -> Self{ + self.farmer_id = Some(farmer_id.into()); + self + } + + pub fn farmer_name(mut self, farmer_name: impl Into) -> Self { + self.farmer_name = Some(farmer_name.into()); + self + } + + pub fn slice_name(mut self, slice_name: impl Into) -> Self { + self.slice_name = Some(slice_name.into()); + self + } + + pub fn cpu_cores(mut self, cpu_cores: i32) -> Self { + self.cpu_cores = Some(cpu_cores); + self + } + + pub fn memory_gb(mut self, memory_gb: i32) -> Self { + self.memory_gb = Some(memory_gb); + self + } + + pub fn storage_gb(mut self, storage_gb: i32) -> Self { + self.storage_gb = Some(storage_gb); + self + } + + pub fn bandwidth_mbps(mut self, bandwidth_mbps: i32) -> Self { + self.bandwidth_mbps = Some(bandwidth_mbps); + self + } + + pub fn min_uptime_sla(mut self, min_uptime_sla: f32) -> Self { + self.min_uptime_sla = Some(min_uptime_sla); + self + } + + pub fn public_ips(mut self, public_ips: i32) -> Self { + self.public_ips = Some(public_ips); + self + } + + pub fn node_id(mut self, node_id: &str, name: &str) -> Self{ + self.node_id = Some(node_id.into()); + self + } + + pub fn slice_type(mut self, slice_type: crate::models::tfmarketplace::product::SliceType) -> Self { + self.slice_type = Some(slice_type); + self + } + + pub fn price_per_hour(mut self, price_per_hour: rust_decimal::Decimal) -> Self { + self.price_per_hour = Some(price_per_hour); + self + } + + pub fn build(self) -> Result { + let farmer_id = self.farmer_id.ok_or("farmer_id is required")?; + let farmer_name = self.farmer_name.ok_or("farmer_name is required")?; + let slice_name = self.slice_name.ok_or("slice_name is required")?; + let cpu_cores = self.cpu_cores.ok_or("cpu_cores is required")?; + let memory_gb = self.memory_gb.ok_or("memory_gb is required")?; + let storage_gb = self.storage_gb.ok_or("storage_gb is required")?; + let bandwidth_mbps = self.bandwidth_mbps.ok_or("bandwidth_mbps is required")?; + let price_per_hour = self.price_per_hour.ok_or("price_per_hour is required")?; + + let slice_config = crate::models::tfmarketplace::product::SliceConfiguration { + cpu_cores, + memory_gb, + storage_gb, + bandwidth_mbps, + min_uptime_sla: self.min_uptime_sla.unwrap_or(99.0), + public_ips: self.public_ips.unwrap_or(0), + node_base_data: BaseModelData::new(), + // id: self.node_id - moved to base_data, + slice_type: self.slice_type.unwrap_or(crate::models::tfmarketplace::product::SliceType::Basic), + pricing: crate::models::tfmarketplace::product::SlicePricing::from_hourly( + price_per_hour, + 5.0, // 5% daily discount + 15.0, // 15% monthly discount + 25.0 // 25% yearly discount + ), + }; + + Ok(crate::models::tfmarketplace::product::Product::create_slice_product( + farmer_id, + farmer_name, + slice_name, + slice_config, + price_per_hour, + )) + } +} diff --git a/heromodels/src/models/tfmarketplace/user.rs b/heromodels/src/models/tfmarketplace/user.rs new file mode 100644 index 0000000..d97b9d2 --- /dev/null +++ b/heromodels/src/models/tfmarketplace/user.rs @@ -0,0 +1,3509 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize, Deserializer}; +use rust_decimal::Decimal; +use std::str::FromStr; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::CustomType; + +/// Represents a user in the system +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +pub struct User { + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + /// User's full name + pub name: String, + /// User's email address + #[index] + pub email: String, + /// User's role in the system + pub role: UserRole, + /// User's country + pub country: Option, + /// User's timezone + pub timezone: Option, + /// Mock data for dashboard + pub mock_data: Option, +} + +/// Represents the possible roles a user can have +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum UserRole { + /// Regular user with limited permissions + User, + /// Administrator with full permissions + Admin, +} + +impl User { + /// Creates a new user with default values + pub fn new(name: String, email: String) -> Self { + Self { + base_data: BaseModelData::new(), + name, + email, + role: UserRole::User, + country: None, + timezone: None, + mock_data: None, + } + } +} + +/// Mock user data for testing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct MockUserData { + pub active_deployments: i32, + pub active_slices: i32, + pub current_cost: i32, + pub balance: i32, + pub wallet_balance_usd: rust_decimal::Decimal, + pub owned_product_ids: Vec, + pub active_rentals: Vec, + pub transaction_history: Vec, + pub resource_utilization: ResourceUtilization, + pub usd_usage_trend: Vec, + pub user_activity: UserActivityStats, + pub recent_activities: Vec, + pub deployment_distribution: DeploymentDistribution, + // Farmer-specific data + pub farmer_data: Option, + // App Provider-specific data + pub app_provider_data: Option, + // Service Provider-specific data + pub service_provider_data: Option, + // Customer Service-specific data + pub customer_service_data: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ResourceUtilization { + pub cpu: i32, + pub memory: i32, + pub storage: i32, + pub network: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeCapacity { + pub cpu_cores: i32, + pub memory_gb: i32, + pub storage_gb: i32, // Keep for backward compatibility + pub bandwidth_mbps: i32, + // Enhanced storage breakdown for GridProxy integration + #[serde(default)] + pub ssd_storage_gb: i32, // SSD storage in GB + #[serde(default)] + pub hdd_storage_gb: i32, // HDD storage in GB +} + +/// Enhanced Node structure for farmer dashboard with modern types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmNode { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub name: String, + pub location: String, + #[serde(deserialize_with = "deserialize_node_status")] + pub status: NodeStatus, + pub capacity: NodeCapacity, + pub used_capacity: NodeCapacity, + pub uptime_percentage: f32, + #[serde(deserialize_with = "deserialize_earnings", alias = "earnings_today")] + pub earnings_today_usd: rust_decimal::Decimal, + #[serde(deserialize_with = "deserialize_datetime")] + pub last_seen: DateTime, + pub health_score: f32, + pub region: String, + pub node_type: String, // "3Node", "Gateway", "Compute" + #[serde(default)] + pub slice_formats: Option>, // Allocated slice format IDs (DEPRECATED) + /// Node rental configuration options + #[serde(default)] + pub rental_options: Option, + /// Current rental availability status + #[serde(default)] + pub availability_status: NodeAvailabilityStatus, + + // NEW: ThreeFold Grid Integration + /// ThreeFold Grid Node ID (e.g., 8, 42, 1337) + #[serde(default)] + pub grid_node_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + /// Data fetched from ThreeFold Grid + #[serde(default)] + pub grid_data: Option, + /// Node group this node belongs to + #[serde(default)] + pub node_group_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + /// When the node was assigned to its current group + #[serde(default)] + pub group_assignment_date: Option>, + /// Shared slice format when in a group (legacy - will be removed) + #[serde(default)] + pub group_slice_format: Option, + /// Shared slice price when in a group (legacy - will be removed) + #[serde(default)] + pub group_slice_price: Option, + + // NEW: Node Staking Configuration + /// Node staking configuration options + #[serde(default)] + pub staking_options: Option, + + // NEW: Marketplace SLA Configuration + /// Marketplace SLA - what the farmer promises to customers (separate from grid reality) + #[serde(default)] + pub marketplace_sla: Option, + + // NEW: Automatic Slice Management + /// Total number of base slices this node can provide + #[serde(default)] + pub total_base_slices: u32, + /// Number of base slices currently allocated/rented + #[serde(default)] + pub allocated_base_slices: u32, + /// Current slice allocations (active rentals) + #[serde(default)] + pub slice_allocations: Vec, + /// Available slice combinations for rental + #[serde(default)] + pub available_combinations: Vec, + /// Slice pricing configuration + #[serde(default)] + pub slice_pricing: crate::services::slice_calculator::SlicePricing, + /// When slice data was last calculated + #[serde(default)] + pub slice_last_calculated: Option>, +} + +// Custom deserializer for NodeStatus from string +fn deserialize_node_status<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.as_str() { + "Online" => Ok(NodeStatus::Online), + "Offline" => Ok(NodeStatus::Offline), + "Maintenance" => Ok(NodeStatus::Maintenance), + "Error" => Ok(NodeStatus::Error), + "Standby" => Ok(NodeStatus::Standby), + _ => Ok(NodeStatus::Online), // Default fallback + } +} + +// Custom deserializer for earnings that handles both number and string +fn deserialize_earnings<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + use serde::de::Error; + + #[derive(Deserialize)] + #[serde(untagged)] + enum EarningsValue { + Number(f64), + String(String), + } + + let value = EarningsValue::deserialize(deserializer)?; + match value { + EarningsValue::Number(n) => { + Decimal::from_str(&n.to_string()).map_err(D::Error::custom) + } + EarningsValue::String(s) => { + Decimal::from_str(&s).map_err(D::Error::custom) + } + } +} + +// Custom deserializer for DateTime from string +fn deserialize_datetime<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error; + let s = String::deserialize(deserializer)?; + DateTime::parse_from_rfc3339(&s) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(D::Error::custom) +} + +// Custom deserializer for optional DateTime from string +fn deserialize_datetime_optional<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error; + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(s) => DateTime::parse_from_rfc3339(&s) + .map(|dt| dt.with_timezone(&Utc)) + .map_err(D::Error::custom), + None => Ok(Utc::now()), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EarningsRecord { + pub date: String, + pub amount: i32, + pub source: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeStatus { + Online, + Offline, + Maintenance, + Error, + Standby, +} + +impl std::fmt::Display for NodeStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NodeStatus::Online => write!(f, "Online"), + NodeStatus::Offline => write!(f, "Offline"), + NodeStatus::Maintenance => write!(f, "Maintenance"), + NodeStatus::Error => write!(f, "Error"), + NodeStatus::Standby => write!(f, "Standby"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmerSettings { + #[serde(default)] + pub auto_accept_deployments: bool, + #[serde(default = "default_maintenance_window")] + pub maintenance_window: String, + #[serde(default)] + pub notification_preferences: NotificationSettings, + pub minimum_deployment_duration: i32, // hours + pub preferred_regions: Vec, + #[serde(default)] + pub default_slice_customizations: Option>, +} + +fn default_maintenance_window() -> String { + "02:00-04:00 UTC".to_string() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationSettings { + #[serde(alias = "email", default)] + pub email_enabled: bool, + #[serde(alias = "sms", default)] + pub sms_enabled: bool, + #[serde(default)] + pub push: Option, + #[serde(default)] + pub node_offline_alerts: Option, + #[serde(default)] + pub earnings_reports: Option, + #[serde(default)] + pub maintenance_reminders: Option, +} + +impl Default for NotificationSettings { + fn default() -> Self { + Self { + email_enabled: true, + sms_enabled: false, + push: Some(true), + node_offline_alerts: Some(true), + earnings_reports: Some(true), + maintenance_reminders: Some(true), + } + } +} + +/// Marketplace SLA configuration - what the farmer promises to customers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketplaceSLA { + /// Uptime guarantee percentage (e.g., 99.95%) + pub uptime_guarantee_percentage: f32, + /// Bandwidth guarantee in Mbps (e.g., 123) + pub bandwidth_guarantee_mbps: i32, + /// Base slice price in USD per hour (e.g., 0.53) + pub base_slice_price: rust_decimal::Decimal, + /// When this SLA was last updated + pub last_updated: chrono::DateTime, +} + +impl Default for MarketplaceSLA { + fn default() -> Self { + Self { + uptime_guarantee_percentage: 99.0, + bandwidth_guarantee_mbps: 100, + base_slice_price: rust_decimal::Decimal::new(50, 2), // $0.50 USD + last_updated: chrono::Utc::now(), + } + } +} + +/// Node rental options that farmers can configure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeRentalOptions { + /// Whether slice rentals are allowed + pub slice_rental_enabled: bool, + /// Whether full node rentals are allowed + pub full_node_rental_enabled: bool, + /// Full node rental pricing configuration + pub full_node_pricing: Option, + /// Minimum rental duration in days + pub minimum_rental_days: u32, + /// Maximum rental duration in days (None = unlimited) + pub maximum_rental_days: Option, + /// Auto-renewal settings + pub auto_renewal_enabled: bool, +} + +impl Default for NodeRentalOptions { + fn default() -> Self { + Self { + slice_rental_enabled: true, + full_node_rental_enabled: false, + full_node_pricing: None, + minimum_rental_days: 30, + maximum_rental_days: None, + auto_renewal_enabled: false, + } + } +} + +/// Node staking options that farmers can configure +/// Staking is used for slashing protection, not for discounts +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeStakingOptions { + /// Whether staking is enabled for this node + pub staking_enabled: bool, + /// Amount of USD Credits staked on this node + pub staked_amount: Decimal, + /// When staking started + pub staking_start_date: Option>, + /// Staking period in months (3, 6, 12, 24) + pub staking_period_months: u32, + /// Whether staking can be withdrawn early (with penalty) + pub early_withdrawal_allowed: bool, + /// Early withdrawal penalty percentage (0-50) + pub early_withdrawal_penalty_percent: f32, +} + +impl Default for NodeStakingOptions { + fn default() -> Self { + Self { + staking_enabled: false, + staked_amount: Decimal::ZERO, + staking_start_date: None, + staking_period_months: 12, + early_withdrawal_allowed: true, + early_withdrawal_penalty_percent: 25.0, + } + } +} + +impl NodeStakingOptions { + /// Create a new builder for NodeStakingOptions + pub fn builder() -> crate::models::builders::NodeStakingOptionsBuilder { + crate::models::builders::NodeStakingOptionsBuilder::new() + } + + /// Check if staking period has ended + pub fn is_staking_period_ended(&self) -> bool { + if let Some(start_date) = self.staking_start_date { + let end_date = start_date + chrono::Duration::days((self.staking_period_months * 30) as i64); + Utc::now() >= end_date + } else { + false + } + } + + /// Get remaining days in staking period + pub fn days_remaining(&self) -> i64 { + if let Some(start_date) = self.staking_start_date { + let end_date = start_date + chrono::Duration::days((self.staking_period_months * 30) as i64); + (end_date - Utc::now()).num_days().max(0) + } else { + 0 + } + } + + /// Calculate early withdrawal penalty amount + pub fn calculate_early_withdrawal_penalty(&self) -> Decimal { + if self.early_withdrawal_allowed && !self.is_staking_period_ended() { + self.staked_amount * Decimal::from_f32_retain(self.early_withdrawal_penalty_percent).unwrap_or(Decimal::ZERO) / Decimal::from(100) + } else { + Decimal::ZERO + } + } +} + +/// Full node rental pricing configuration with auto-calculation support +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FullNodePricing { + /// Hourly rate in USD + pub hourly: Decimal, + /// Daily rate in USD + pub daily: Decimal, + /// Monthly rate in USD + pub monthly: Decimal, + /// Yearly rate in USD + pub yearly: Decimal, + /// Whether to auto-calculate rates from hourly base + pub auto_calculate: bool, + /// Daily discount percentage (0-50) + pub daily_discount_percent: f32, + /// Monthly discount percentage (0-50) + pub monthly_discount_percent: f32, + /// Yearly discount percentage (0-50) + pub yearly_discount_percent: f32, +} + +impl Default for FullNodePricing { + fn default() -> Self { + Self { + hourly: Decimal::from(0), + daily: Decimal::from(0), + monthly: Decimal::from(0), + yearly: Decimal::from(0), + auto_calculate: true, + daily_discount_percent: 0.0, + monthly_discount_percent: 0.0, + yearly_discount_percent: 0.0, + } + } +} + +impl FullNodePricing { + /// Create a new builder for FullNodePricing + pub fn builder() -> crate::models::builders::FullNodePricingBuilder { + crate::models::builders::FullNodePricingBuilder::new() + } + + /// Calculate all rates from hourly base rate with discounts + pub fn calculate_from_hourly(&mut self) { + if !self.auto_calculate { + return; + } + + let hourly_f64 = self.hourly.to_string().parse::().unwrap_or(0.0); + + // Calculate base rates + let base_daily = hourly_f64 * 24.0; + let base_monthly = hourly_f64 * 24.0 * 30.0; + let base_yearly = hourly_f64 * 24.0 * 365.0; + + // Apply discounts (convert f32 to f64 for calculations) + let daily_rate = base_daily * (1.0 - self.daily_discount_percent as f64 / 100.0); + let monthly_rate = base_monthly * (1.0 - self.monthly_discount_percent as f64 / 100.0); + let yearly_rate = base_yearly * (1.0 - self.yearly_discount_percent as f64 / 100.0); + + // Update rates + self.daily = Decimal::from_str(&daily_rate.to_string()).unwrap_or(Decimal::from(0)); + self.monthly = Decimal::from_str(&monthly_rate.to_string()).unwrap_or(Decimal::from(0)); + self.yearly = Decimal::from_str(&yearly_rate.to_string()).unwrap_or(Decimal::from(0)); + } +} + +impl NodeRentalOptions { + /// Create a new builder for NodeRentalOptions + pub fn builder() -> crate::models::builders::NodeRentalOptionsBuilder { + crate::models::builders::NodeRentalOptionsBuilder::new() + } +} + +/// Node availability status for rental management +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum NodeAvailabilityStatus { + /// Node is fully available for any rental type + Available, + /// Node has some slices rented but capacity remains + PartiallyRented, + /// Node is fully rented (either full node or all slices) + FullyRented, + /// Node is offline or in maintenance + Unavailable, + /// Node is reserved for specific customer + Reserved, +} + +impl Default for NodeAvailabilityStatus { + fn default() -> Self { + Self::Available + } +} + +/// Individual node rental record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeRental { + pub base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub node_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub renter_email: String, + pub rental_type: NodeRentalType, + pub monthly_cost: Decimal, + pub start_date: DateTime, + pub end_date: DateTime, + pub status: NodeRentalStatus, + pub auto_renewal: bool, + pub payment_method: String, + pub metadata: std::collections::HashMap, +} + +/// Type of node rental +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeRentalType { + /// Renting specific slices of the node + Slice { + slice_ids: Vec, + total_cpu_cores: u32, + total_memory_gb: u32, + total_storage_gb: u32, + }, + /// Renting the entire node exclusively + FullNode, +} + +/// Status of a node rental +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NodeRentalStatus { + Active, + Pending, + Expired, + Cancelled, + Suspended, +} + +/// Farmer earnings from node rentals +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FarmerRentalEarning { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub node_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub rental_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub renter_email: String, + pub amount: Decimal, + pub currency: String, + pub earning_date: DateTime, + pub rental_type: NodeRentalType, + pub payment_status: PaymentStatus, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentStatus { + Pending, + Completed, + Failed, + Refunded, +} + +impl NodeRental { + /// Create a new builder for NodeRental + pub fn builder() -> crate::models::builders::NodeRentalBuilder { + crate::models::builders::NodeRentalBuilder::new() + } + + /// Check if rental is currently active + pub fn is_active(&self) -> bool { + matches!(self.status, NodeRentalStatus::Active) && + Utc::now() >= self.start_date && + Utc::now() <= self.end_date + } + + /// Get remaining days in rental + pub fn days_remaining(&self) -> i64 { + (self.end_date - Utc::now()).num_days() + } + + /// Calculate total rental cost + pub fn total_cost(&self) -> Decimal { + let months = (self.end_date - self.start_date).num_days() as f64 / 30.0; + self.monthly_cost * Decimal::try_from(months).unwrap_or(Decimal::ZERO) + } +} + +impl FarmerRentalEarning { + /// Create a new builder for FarmerRentalEarning + pub fn builder() -> crate::models::builders::FarmerRentalEarningBuilder { + crate::models::builders::FarmerRentalEarningBuilder::new() + } +} + +impl FarmNode { + /// Create a new builder for FarmNode + pub fn builder() -> crate::models::builders::FarmNodeBuilder { + crate::models::builders::FarmNodeBuilder::new() + } +} + +impl NodeGroup { + /// Create a new builder for NodeGroup + pub fn builder() -> crate::models::builders::NodeGroupBuilder { + crate::models::builders::NodeGroupBuilder::new() + } +} + +impl GridNodeData { + /// Create a new builder for GridNodeData + pub fn builder() -> crate::models::tfmarketplace::builders::GridNodeDataBuilder { + crate::models::tfmarketplace::builders::GridNodeDataBuilder::new() + } +} + +/// User Activity Tracking +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserActivity { + pub base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub activity_type: ActivityType, + pub description: String, + #[serde(deserialize_with = "deserialize_datetime")] + pub timestamp: DateTime, + pub metadata: std::collections::HashMap, + pub category: String, + pub importance: ActivityImportance, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ActivityType { + Login, + Purchase, + Deployment, + ServiceCreated, + AppPublished, + NodeAdded, + NodeUpdated, + WalletTransaction, + ProfileUpdate, + SettingsChange, + MarketplaceView, + SliceCreated, + SliceAllocated, + SliceReleased, + SliceRentalStarted, + SliceRentalStopped, + SliceRentalRestarted, + SliceRentalCancelled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ActivityImportance { + Low, + Medium, + High, + Critical, +} + +/// Enhanced User Statistics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UsageStatistics { + #[serde(default)] + pub total_deployments: i32, + #[serde(default)] + pub active_services: i32, + #[serde(default)] + pub total_spent: rust_decimal::Decimal, + #[serde(default)] + pub favorite_categories: Vec, + #[serde(default)] + pub usage_trends: Vec, + #[serde(default = "default_login_frequency")] + pub login_frequency: f32, // logins per week + #[serde(default)] + pub preferred_regions: Vec, + #[serde(default)] + pub account_age_days: i32, + #[serde(deserialize_with = "deserialize_datetime_optional", default = "default_last_activity")] + pub last_activity: DateTime, +} + +fn default_login_frequency() -> f32 { + 1.0 +} + +fn default_last_activity() -> DateTime { + Utc::now() +} + +impl Default for UsageStatistics { + fn default() -> Self { + Self { + total_deployments: 0, + active_services: 0, + total_spent: rust_decimal::Decimal::ZERO, + favorite_categories: Vec::new(), + usage_trends: Vec::new(), + login_frequency: 1.0, + preferred_regions: Vec::new(), + account_age_days: 0, + last_activity: Utc::now(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UsageTrend { + pub period: String, // "week", "month", "quarter" + pub metric: String, // "deployments", "spending", "logins" + pub value: f32, + pub change_percentage: f32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserPreferences { + #[serde(alias = "currency_display")] + pub preferred_currency: String, + #[serde(alias = "language")] + pub preferred_language: String, + pub timezone: String, + pub dashboard_layout: String, + #[serde(alias = "notifications")] + pub notification_settings: NotificationSettings, + #[serde(alias = "privacy")] + pub privacy_settings: PrivacySettings, + #[serde(default)] + pub theme: Option, + #[serde(default)] + pub last_payment_method: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrivacySettings { + #[serde(default)] + pub profile_visibility: Option, // "public", "private", "friends" + #[serde(alias = "analytics")] + pub activity_tracking: bool, + #[serde(alias = "marketing")] + pub marketing_emails: bool, + pub data_sharing: bool, +} + +/// Enhanced User Dashboard Data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserDashboardData { + pub user_info: UserInfo, + pub recent_activities: Vec, + pub usage_statistics: Option, + pub active_services: Vec, + pub active_deployments: i32, + pub wallet_summary: WalletSummary, + pub recommendations: Vec, + pub quick_actions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserInfo { + pub name: String, + pub email: String, + pub member_since: String, + pub account_type: String, + pub verification_status: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WalletSummary { + pub balance: rust_decimal::Decimal, + pub currency: String, + pub recent_transactions: i32, + pub pending_transactions: i32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Recommendation { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub title: String, + pub description: String, + pub action_url: String, + pub priority: String, + pub category: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuickAction { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub title: String, + pub description: String, + pub action_url: String, + pub icon: String, + pub enabled: bool, +} + +/// App Provider-specific data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppProviderData { + pub published_apps: i32, + pub total_deployments: i32, + pub active_deployments: i32, + pub monthly_revenue_usd: i32, + pub total_revenue_usd: i32, + pub apps: Vec, + pub deployment_stats: Vec, + pub revenue_history: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PublishedApp { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub name: String, + pub category: String, + pub version: String, + pub status: String, + pub deployments: i32, + pub rating: f32, + pub monthly_revenue_usd: i32, + pub last_updated: String, + #[serde(default)] + pub auto_healing: Option, +} + +impl PublishedApp { + pub fn builder() -> Self { + Self { + base_data: BaseModelData::new(), + // id: String::new() - moved to base_data, + name: String::new(), + category: String::new(), + version: "1.0.0".to_string(), + status: "Active".to_string(), + deployments: 0, + rating: 0.0, + monthly_revenue_usd: 0, + last_updated: chrono::Utc::now().format("%Y-%m-%d").to_string(), + auto_healing: Some(false), + } + } + + pub fn analytics_template(name: &str) -> Self { + Self { + base_data: BaseModelData::new(), + name: name.to_string(), + category: "Analytics".to_string(), + version: "1.0.0".to_string(), + status: "Active".to_string(), + deployments: 0, + rating: 4.5, + monthly_revenue_usd: 0, + last_updated: chrono::Utc::now().format("%Y-%m-%d").to_string(), + auto_healing: Some(true), + } + } + + pub fn database_template(name: &str) -> Self { + Self { + base_data: BaseModelData::new(), + name: name.to_string(), + category: "Database".to_string(), + version: "1.0.0".to_string(), + status: "Active".to_string(), + deployments: 0, + rating: 4.2, + monthly_revenue_usd: 0, + last_updated: chrono::Utc::now().format("%Y-%m-%d").to_string(), + auto_healing: Some(true), + } + } + + pub fn web_template(name: &str) -> Self { + Self { + base_data: BaseModelData::new(), + name: name.to_string(), + category: "Web".to_string(), + version: "1.0.0".to_string(), + status: "Active".to_string(), + deployments: 0, + rating: 4.0, + monthly_revenue_usd: 0, + last_updated: chrono::Utc::now().format("%Y-%m-%d").to_string(), + auto_healing: Some(true), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeploymentStat { + pub app_name: String, + pub region: String, + pub instances: i32, + pub status: String, + pub resource_usage: ResourceUtilization, + #[serde(default)] + pub customer_name: Option, + #[serde(default)] + pub deployed_date: Option, + #[serde(default)] + pub deployment_base_data: BaseModelData::new(), + // id: Option - moved to base_data, + #[serde(default)] + pub auto_healing: Option, +} + +impl DeploymentStat { + pub fn builder() -> Self { + Self { + app_name: String::new(), + region: String::new(), + instances: 0, + status: "Running".to_string(), + resource_usage: ResourceUtilization { + cpu: 0, + memory: 0, + storage: 0, + network: 0, + }, + customer_name: None, + deployed_date: Some(chrono::Utc::now().format("%Y-%m-%d").to_string()), + deployment_base_data: BaseModelData::new(), + // id: None - moved to base_data, + auto_healing: Some(false), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RevenueRecord { + pub date: String, + pub amount: i32, + pub app_name: String, +} + +/// Transaction record for wallet operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub transaction_type: TransactionType, + pub amount: rust_decimal::Decimal, + pub timestamp: DateTime, + pub status: TransactionStatus, +} + +/// Types of transactions +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TransactionType { + Purchase { + product_id: String, + }, + Rental { + product_id: String, + duration: String + }, + Transfer { to_user: String }, + Earning { source: String }, + // Instant purchase transaction + InstantPurchase { + product_id: String, + order_id: String, + purchase_method: String + }, + // Pool-related transactions (now USD-based) + Exchange { + pool_id: String, + from_token: String, + to_token: String, + from_amount_usd: rust_decimal::Decimal, + to_amount_usd: rust_decimal::Decimal + }, + Stake { + amount_usd: rust_decimal::Decimal, + duration_months: u32 + }, + Unstake { + amount_usd: rust_decimal::Decimal + }, + // Auto top-up transaction type (USD-based) + AutoTopUp { + triggered_by_purchase: Option, + threshold_amount_usd: rust_decimal::Decimal, + amount_usd: rust_decimal::Decimal, + payment_method: String + }, + // Credits transaction types (USD-based) + CreditsPurchase { payment_method: String }, + CreditsSale { + currency: String, + fiat_amount: rust_decimal::Decimal, + payout_method: String + }, + CreditsTransfer { to_user: String, note: Option }, +} + +/// Transaction status +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TransactionStatus { + Pending, + Completed, + Failed, + Cancelled, +} + +/// Rental record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Rental { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub product_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub start_date: DateTime, + pub end_date: Option>, + pub status: RentalStatus, + pub monthly_cost: rust_decimal::Decimal, +} + +/// Rental status +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RentalStatus { + Active, + Expired, + Cancelled, + Pending, +} + +impl MockUserData { + pub fn new_user() -> Self { + Self { + active_deployments: 0, + active_slices: 0, + current_cost: 0, + balance: 0, + wallet_balance_usd: rust_decimal_macros::dec!(0.0), // Start with 0 USD + owned_product_ids: vec![], + active_rentals: vec![], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: uuid::Uuid::new_v4().to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "new_user".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "account_creation".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(0.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-06T12:00:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 0, + memory: 0, + storage: 0, + network: 0, + }, + usd_usage_trend: vec![0, 0, 0, 0, 0, 0], + user_activity: UserActivityStats { + deployments: vec![0, 0, 0, 0, 0, 0], + resource_reservations: vec![0, 0, 0, 0, 0, 0], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-06".to_string(), + action: "Account Created".to_string(), + status: "Completed".to_string(), + details: "Welcome to ThreeFold!".to_string(), + } + ], + deployment_distribution: DeploymentDistribution { + regions: vec![], // New users have no deployments + }, + farmer_data: None, + app_provider_data: None, + service_provider_data: None, + customer_service_data: None, + } + } + + pub fn user1() -> Self { + Self { + active_deployments: 12, + active_slices: 20, + current_cost: 850, + balance: 8500, + wallet_balance_usd: rust_decimal_macros::dec!(8500.0), + owned_product_ids: vec![ + "compute_001".to_string(), + "compute_002".to_string(), + "hardware_001".to_string(), + "hardware_002".to_string(), + "app_001".to_string(), + "app_002".to_string(), + "service_001".to_string(), + ], + active_rentals: vec![ + "rental_001".to_string(), + "rental_002".to_string(), + "rental_003".to_string(), + "rental_004".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_001".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "Node TF-EU-001".to_string() + }, + amount: rust_decimal_macros::dec!(45.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-06T14:21:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_002".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "App Revenue".to_string() + }, + amount: rust_decimal_macros::dec!(120.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-05T16:30:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_003".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "Service Revenue".to_string() + }, + amount: rust_decimal_macros::dec!(180.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-04T18:45:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_004".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "credit_purchase".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(2000.0), + timestamp: DateTime::parse_from_rfc3339("2025-04-26T15:45:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 75, + memory: 82, + storage: 88, + network: 65, + }, + usd_usage_trend: vec![400, 520, 650, 720, 780, 850], + user_activity: UserActivityStats { + deployments: vec![8, 10, 11, 12, 14, 12], + resource_reservations: vec![12, 15, 18, 20, 22, 20], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-06".to_string(), + action: "Node Earnings".to_string(), + status: "Completed".to_string(), + details: "Received $45 USD Credits from node TF-EU-001".to_string(), + }, + RecentActivity { + date: "2025-05-05".to_string(), + action: "App Revenue".to_string(), + status: "Completed".to_string(), + details: "Earned $120 USD Credits from AI Analytics Platform deployments".to_string(), + }, + RecentActivity { + date: "2025-05-04".to_string(), + action: "Service Completed".to_string(), + status: "Completed".to_string(), + details: "Completed DevOps consultation, earned $180 USD Credits".to_string(), + }, + RecentActivity { + date: "2025-05-03".to_string(), + action: "Node Upgrade".to_string(), + status: "Completed".to_string(), + details: "Upgraded 3Node hardware configuration".to_string(), + }, + RecentActivity { + date: "2025-05-02".to_string(), + action: "App Update".to_string(), + status: "Completed".to_string(), + details: "Updated Database Cluster Pro to v1.5.2".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 2, + slices: 8, + apps: 5, + gateways: 2, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 1, + slices: 7, + apps: 4, + gateways: 1, + }, + RegionDeployments { + region: "Asia".to_string(), + nodes: 0, + slices: 5, + apps: 3, + gateways: 1, + }, + ], + }, + // Enhanced with Farmer Data + farmer_data: Some(FarmerData { + total_nodes: 3, + online_nodes: 3, + total_capacity: NodeCapacity { + cpu_cores: 48, + memory_gb: 192, + storage_gb: 6000, + bandwidth_mbps: 1000, + ssd_storage_gb: 3000, + hdd_storage_gb: 3000, + }, + used_capacity: NodeCapacity { + cpu_cores: 22, + memory_gb: 98, + storage_gb: 4100, + bandwidth_mbps: 350, + ssd_storage_gb: 2100, + hdd_storage_gb: 2000, + }, + monthly_earnings_usd: 1250, + total_earnings_usd: 18750, + uptime_percentage: 99.2, + nodes: vec![ + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-EU-001".to_string() - moved to base_data, + name: "Frankfurt Node".to_string(), + location: "Frankfurt, Germany".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 8, + memory_gb: 32, + storage_gb: 1400, + bandwidth_mbps: 150, + ssd_storage_gb: 700, + hdd_storage_gb: 700, + }, + uptime_percentage: 99.8, + earnings_today_usd: rust_decimal::Decimal::from(45), + last_seen: chrono::Utc::now(), + health_score: 98.5, + region: "EU".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 2, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-EU-002".to_string() - moved to base_data, + name: "Amsterdam Node".to_string(), + location: "Amsterdam, Netherlands".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 7, + memory_gb: 28, + storage_gb: 1200, + bandwidth_mbps: 100, + ssd_storage_gb: 600, + hdd_storage_gb: 600, + }, + uptime_percentage: 98.9, + earnings_today_usd: rust_decimal::Decimal::from(38), + last_seen: chrono::Utc::now(), + health_score: 97.2, + region: "EU".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 1, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-NA-002".to_string() - moved to base_data, + name: "Toronto Node".to_string(), + location: "Toronto, Canada".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 7, + memory_gb: 38, + storage_gb: 1500, + bandwidth_mbps: 100, + ssd_storage_gb: 800, + hdd_storage_gb: 700, + }, + uptime_percentage: 99.1, + earnings_today_usd: rust_decimal::Decimal::from(42), + last_seen: chrono::Utc::now(), + health_score: 98.8, + region: "NA".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 1, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + ], + earnings_history: vec![ + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 115, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 128, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 122, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 135, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 132, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 118, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 125, + source: "Node Operations".to_string(), + }, + ], + slice_templates: Vec::new(), + active_slices: 0, + }), + // Enhanced with App Provider Data + app_provider_data: Some(AppProviderData { + published_apps: 3, + total_deployments: 85, + active_deployments: 12, + monthly_revenue_usd: 1850, + total_revenue_usd: 27750, + apps: vec![ + PublishedApp::analytics_template("app-001", "AI Analytics Platform") + .with_stats(35, 4.8, 950), + PublishedApp::database_template("app-002", "Database Cluster Pro") + .with_stats(28, 4.6, 650) + .with_auto_healing(false), + PublishedApp::web_template("app-003", "Web Server Stack") + .with_stats(22, 4.4, 250), + ], + deployment_stats: vec![ + DeploymentStat::builder() + .app_name("AI Analytics Platform") + .region("Europe") + .instances(5) + .status("Running") + .resource_usage(ResourceUtilization { + cpu: 85, + memory: 78, + storage: 92, + network: 65, + }) + .auto_healing(true) + .build() + .unwrap(), + DeploymentStat::builder() + .app_name("Database Cluster Pro") + .region("North America") + .instances(4) + .status("Running") + .resource_usage(ResourceUtilization { + cpu: 72, + memory: 88, + storage: 95, + network: 45, + }) + .auto_healing(false) + .build() + .unwrap(), + ], + revenue_history: vec![ + RevenueRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 75, + app_name: "AI Analytics Platform".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 68, + app_name: "Database Cluster Pro".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 52, + app_name: "Web Server Stack".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 85, + app_name: "AI Analytics Platform".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 62, + app_name: "Database Cluster Pro".to_string(), + }, + ], + }), + // Enhanced with Service Provider Data + service_provider_data: Some(ServiceProviderData { + active_services: 0, + total_clients: 0, + monthly_revenue_usd: 0, + total_revenue_usd: 0, + service_rating: 4.5, + services: Vec::new(), // Services will be loaded from persistent storage + client_requests: vec![ + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-001".to_string() - moved to base_data, + client_name: "TechCorp Inc.".to_string(), + service_name: "DevOps Consultation".to_string(), + status: "In Progress".to_string(), + requested_date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + estimated_hours: 30, + budget: 2550, + priority: "High".to_string(), + progress: Some(60), // In Progress requests have some progress + completed_date: None, + client_email: Some("contact@techcorp.com".to_string()), + client_phone: Some("+1-555-0100".to_string()), + description: Some("DevOps consultation for infrastructure optimization and CI/CD pipeline setup.".to_string()), + created_date: Some((Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string()), + }, + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-002".to_string() - moved to base_data, + client_name: "StartupXYZ".to_string(), + service_name: "Cloud Migration".to_string(), + status: "Active".to_string(), + requested_date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + estimated_hours: 50, + budget: 4750, + priority: "Medium".to_string(), + progress: Some(30), // Active requests have some progress + completed_date: None, + client_email: Some("tech@startupxyz.com".to_string()), + client_phone: Some("+1-555-0200".to_string()), + description: Some("Complete cloud migration from on-premises infrastructure to AWS with security and performance optimization.".to_string()), + created_date: Some((Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string()), + }, + ], + revenue_history: vec![ + RevenueRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 510, + app_name: "DevOps Consultation".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 760, + app_name: "Cloud Migration".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 880, + app_name: "Security Audit".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 595, + app_name: "DevOps Consultation".to_string(), + }, + ], + }), + customer_service_data: Some(CustomerServiceData { + active_bookings: 2, + completed_bookings: 5, + total_spent: 2400, + monthly_spending: 480, + average_rating_given: 4.6, + service_bookings: vec![ + ServiceBooking { + base_data: BaseModelData::new(), + // id: "booking_001".to_string() - moved to base_data, + service_base_data: BaseModelData::new(), + // id: "service_001".to_string() - moved to base_data, + service_name: "Cloud Migration Consulting".to_string(), + provider_email: "user5@example.com".to_string(), + customer_email: "user1@example.com".to_string(), + budget: 800, + estimated_hours: 16, + status: "In Progress".to_string(), + requested_date: "2025-06-20".to_string(), + priority: "High".to_string(), + description: Some("Migrate legacy infrastructure to ThreeFold Grid".to_string()), + booking_date: "2025-06-15".to_string(), + client_phone: Some("+1-555-0123".to_string()), + progress: Some(60), + completed_date: None, + }, + ServiceBooking { + base_data: BaseModelData::new(), + // id: "booking_002".to_string() - moved to base_data, + service_base_data: BaseModelData::new(), + // id: "service_002".to_string() - moved to base_data, + service_name: "DevOps Consultation".to_string(), + provider_email: "user5@example.com".to_string(), + customer_email: "user1@example.com".to_string(), + budget: 600, + estimated_hours: 12, + status: "Pending".to_string(), + requested_date: "2025-06-25".to_string(), + priority: "Medium".to_string(), + description: Some("Setup CI/CD pipeline for ThreeFold deployment".to_string()), + booking_date: "2025-06-18".to_string(), + client_phone: Some("+1-555-0123".to_string()), + progress: None, + completed_date: None, + }, + ], + favorite_providers: vec![ + "user5@example.com".to_string(), + "provider2@example.com".to_string(), + ], + spending_history: vec![ + SpendingRecord { + date: "2025-06-15".to_string(), + amount: 800, + service_name: "Cloud Migration Consulting".to_string(), + provider_name: "Jordan Mitchell".to_string(), + }, + SpendingRecord { + date: "2025-05-20".to_string(), + amount: 400, + service_name: "Security Audit".to_string(), + provider_name: "Security Expert".to_string(), + }, + SpendingRecord { + date: "2025-05-10".to_string(), + amount: 600, + service_name: "Performance Optimization".to_string(), + provider_name: "Performance Specialist".to_string(), + }, + ], + }), + } + } + + pub fn user2() -> Self { + Self { + active_deployments: 1, + active_slices: 2, + current_cost: 85, + balance: 450, + wallet_balance_usd: rust_decimal_macros::dec!(450.0), + owned_product_ids: vec![ + "compute_003".to_string(), + ], + active_rentals: vec![ + "rental_004".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_003".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user2@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Rental { + product_base_data: BaseModelData::new(), + // id: "compute_003".to_string() - moved to base_data, + duration: "monthly".to_string() + }, + amount: rust_decimal_macros::dec!(85.0), + timestamp: DateTime::parse_from_rfc3339("2025-04-27T09:15:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_004".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user2@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "credit_purchase".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(200.0), + timestamp: DateTime::parse_from_rfc3339("2025-04-23T16:20:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 35, + memory: 42, + storage: 28, + network: 15, + }, + usd_usage_trend: vec![50, 65, 70, 75, 80, 85], + user_activity: UserActivityStats { + deployments: vec![1, 2, 1, 1, 2, 3], + resource_reservations: vec![1, 1, 2, 1, 2, 2], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-04-27".to_string(), + action: "App Deployment".to_string(), + status: "Completed".to_string(), + details: "Deployed Database Application".to_string(), + }, + RecentActivity { + date: "2025-04-24".to_string(), + action: "Resource Allocation".to_string(), + status: "Active".to_string(), + details: "Added 1 compute slice".to_string(), + }, + RecentActivity { + date: "2025-04-23".to_string(), + action: "Payment".to_string(), + status: "Completed".to_string(), + details: "Added $200 USD Credits to account".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "North America".to_string(), + nodes: 0, + slices: 2, + apps: 1, + gateways: 0, + }, + ], // Total: 3 items (1 app + 2 slices) matching active_deployments: 1, active_slices: 2 + }, + farmer_data: None, + app_provider_data: None, + service_provider_data: None, + customer_service_data: None, + } + } + + pub fn user4() -> Self { + Self { + active_deployments: 4, + active_slices: 8, + current_cost: 320, + balance: 1200, + wallet_balance_usd: rust_decimal_macros::dec!(1200.0), + owned_product_ids: vec![ + "compute_008".to_string(), + "compute_009".to_string(), + ], + active_rentals: vec![ + "rental_013".to_string(), + "rental_014".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_013".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user4@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Rental { + product_base_data: BaseModelData::new(), + // id: "compute_008".to_string() - moved to base_data, + duration: "monthly".to_string() + }, + amount: rust_decimal_macros::dec!(160.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-02T14:20:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_014".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user4@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "paypal_purchase".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(500.0), + timestamp: DateTime::parse_from_rfc3339("2025-04-28T12:10:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 60, + memory: 55, + storage: 70, + network: 40, + }, + usd_usage_trend: vec![180, 220, 260, 290, 310, 320], + user_activity: UserActivityStats { + deployments: vec![3, 4, 4, 4, 4, 4], + resource_reservations: vec![6, 7, 8, 8, 8, 8], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-02".to_string(), + action: "Resource Allocation".to_string(), + status: "Completed".to_string(), + details: "Allocated additional compute resources".to_string(), + }, + RecentActivity { + date: "2025-04-30".to_string(), + action: "App Deployment".to_string(), + status: "Active".to_string(), + details: "Deployed web application cluster".to_string(), + }, + RecentActivity { + date: "2025-04-28".to_string(), + action: "Payment".to_string(), + status: "Completed".to_string(), + details: "Added $500 USD Credits via PayPal".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 0, + slices: 5, + apps: 2, + gateways: 1, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 0, + slices: 3, + apps: 2, + gateways: 0, + }, + ], + }, + farmer_data: None, + app_provider_data: None, + service_provider_data: None, + customer_service_data: None, + } + } + + pub fn user5() -> Self { + Self { + active_deployments: 6, + active_slices: 14, + current_cost: 580, + balance: 2800, + wallet_balance_usd: rust_decimal_macros::dec!(2800.0), + owned_product_ids: vec![ + "compute_010".to_string(), + "hardware_005".to_string(), + "app_006".to_string(), + ], + active_rentals: vec![ + "rental_015".to_string(), + "rental_016".to_string(), + "rental_017".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_011".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user5@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "hardware_005".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(1500.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-03T16:45:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_015".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user5@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "bank_transfer".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(2000.0), + timestamp: DateTime::parse_from_rfc3339("2025-04-30T10:30:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 75, + memory: 68, + storage: 82, + network: 55, + }, + usd_usage_trend: vec![400, 450, 500, 540, 560, 580], + user_activity: UserActivityStats { + deployments: vec![5, 6, 6, 6, 6, 6], + resource_reservations: vec![10, 12, 14, 14, 14, 14], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-03".to_string(), + action: "Hardware Purchase".to_string(), + status: "Completed".to_string(), + details: "Purchased enterprise hardware package".to_string(), + }, + RecentActivity { + date: "2025-05-01".to_string(), + action: "Service Scaling".to_string(), + status: "Active".to_string(), + details: "Scaled services across multiple regions".to_string(), + }, + RecentActivity { + date: "2025-04-30".to_string(), + action: "Payment".to_string(), + status: "Completed".to_string(), + details: "Added $2000 USD Credits via bank transfer".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 0, + slices: 8, + apps: 3, + gateways: 1, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 0, + slices: 4, + apps: 2, + gateways: 1, + }, + RegionDeployments { + region: "Asia".to_string(), + nodes: 0, + slices: 2, + apps: 1, + gateways: 0, + }, + ], + }, + farmer_data: None, + app_provider_data: None, + service_provider_data: Some(ServiceProviderData { + active_services: 3, + total_clients: 12, + total_revenue_usd: 8500, + monthly_revenue_usd: 1200, + service_rating: 4.7, + services: vec![ + Service { + base_data: BaseModelData::new(), + // id: "svc_001".to_string() - moved to base_data, + name: "Cloud Migration Consulting".to_string(), + description: "Expert cloud migration services for enterprise clients".to_string(), + category: "Consulting".to_string(), + price_per_hour_usd: 85, + status: "Active".to_string(), + rating: 4.8, + clients: 8, + total_hours: 240, + }, + Service { + base_data: BaseModelData::new(), + // id: "svc_002".to_string() - moved to base_data, + name: "DevOps Implementation".to_string(), + description: "Complete DevOps pipeline setup and optimization".to_string(), + category: "Development".to_string(), + price_per_hour_usd: 75, + status: "Active".to_string(), + rating: 4.6, + clients: 5, + total_hours: 180, + }, + Service { + base_data: BaseModelData::new(), + // id: "svc_003".to_string() - moved to base_data, + name: "Security Audit".to_string(), + description: "Comprehensive security assessment and recommendations".to_string(), + category: "Security".to_string(), + price_per_hour_usd: 95, + status: "Active".to_string(), + rating: 4.9, + clients: 3, + total_hours: 120, + }, + ], + client_requests: vec![ + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-12345678-abcd1234".to_string() - moved to base_data, // Matches booking from User1 + client_name: "Sara Nicks".to_string(), + client_email: Some("user1@example.com".to_string()), + service_name: "Cloud Migration Consulting".to_string(), + status: "In Progress".to_string(), + requested_date: "2025-06-20".to_string(), + estimated_hours: 16, + budget: 800, + priority: "High".to_string(), + description: Some("Migration of legacy systems to ThreeFold Grid".to_string()), + created_date: Some("2025-06-15".to_string()), + client_phone: Some("+1-555-0123".to_string()), + progress: Some(65), + completed_date: None, + }, + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-87654321-efgh5678".to_string() - moved to base_data, // Matches booking from User1 + client_name: "Sara Nicks".to_string(), + client_email: Some("user1@example.com".to_string()), + service_name: "DevOps Implementation".to_string(), + status: "Pending".to_string(), + requested_date: "2025-06-25".to_string(), + estimated_hours: 12, + budget: 600, + priority: "Medium".to_string(), + description: Some("Setup CI/CD pipeline for new project".to_string()), + created_date: Some("2025-06-18".to_string()), + client_phone: Some("+1-555-0123".to_string()), + progress: Some(0), + completed_date: None, + }, + ], + revenue_history: vec![ + RevenueRecord { + date: "2025-06-01".to_string(), + amount: 1200, + app_name: "Cloud Migration Consulting".to_string(), + }, + RevenueRecord { + date: "2025-05-01".to_string(), + amount: 900, + app_name: "DevOps Implementation".to_string(), + }, + RevenueRecord { + date: "2025-04-01".to_string(), + amount: 1140, + app_name: "Security Audit".to_string(), + }, + ], + }), + customer_service_data: None, + } + } + + /// Farmer user with comprehensive farming data + pub fn farmer() -> Self { + Self { + active_deployments: 3, + active_slices: 8, + current_cost: 120, + balance: 5800, + wallet_balance_usd: rust_decimal_macros::dec!(5800.0), + owned_product_ids: vec![ + "hardware_001".to_string(), + "hardware_002".to_string(), + "hardware_003".to_string(), + "compute_004".to_string(), + "compute_005".to_string(), + ], + active_rentals: vec![ + "rental_005".to_string(), + "rental_006".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_005".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "farmer1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "Node TF-EU-001".to_string() + }, + amount: rust_decimal_macros::dec!(45.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-05T14:21:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_006".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "farmer1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "Node TF-EU-002".to_string() + }, + amount: rust_decimal_macros::dec!(38.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-04T14:21:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 45, + memory: 52, + storage: 68, + network: 35, + }, + usd_usage_trend: vec![80, 95, 110, 105, 115, 120], + user_activity: UserActivityStats { + deployments: vec![2, 3, 2, 3, 4, 3], + resource_reservations: vec![5, 6, 7, 8, 8, 8], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-05".to_string(), + action: "Node Earnings".to_string(), + status: "Completed".to_string(), + details: "Received $45 USD Credits from node TF-EU-001".to_string(), + }, + RecentActivity { + date: "2025-05-04".to_string(), + action: "Node Online".to_string(), + status: "Active".to_string(), + details: "Node TF-NA-002 came back online".to_string(), + }, + RecentActivity { + date: "2025-05-03".to_string(), + action: "Capacity Upgrade".to_string(), + status: "Completed".to_string(), + details: "Added 2TB storage to TF-EU-001".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 2, + slices: 5, + apps: 2, + gateways: 1, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 1, + slices: 3, + apps: 1, + gateways: 0, + }, + ], + }, + farmer_data: Some(FarmerData { + total_nodes: 3, + online_nodes: 3, + total_capacity: NodeCapacity { + cpu_cores: 48, + memory_gb: 192, + storage_gb: 6000, + bandwidth_mbps: 1000, + ssd_storage_gb: 3000, + hdd_storage_gb: 3000, + }, + used_capacity: NodeCapacity { + cpu_cores: 22, + memory_gb: 98, + storage_gb: 4100, + bandwidth_mbps: 350, + ssd_storage_gb: 2100, + hdd_storage_gb: 2000, + }, + monthly_earnings_usd: 1250, + total_earnings_usd: 18750, + uptime_percentage: 99.2, + nodes: vec![ + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-EU-001".to_string() - moved to base_data, + name: "Frankfurt Node".to_string(), + location: "Frankfurt, Germany".to_string(), + status: crate::models::user::NodeStatus::Online, + uptime_percentage: 99.8, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 8, + memory_gb: 32, + storage_gb: 1400, + bandwidth_mbps: 150, + ssd_storage_gb: 700, + hdd_storage_gb: 700, + }, + earnings_today_usd: rust_decimal::Decimal::from(45), + last_seen: chrono::Utc::now(), + health_score: 98.5, + region: "EU".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 2, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-EU-002".to_string() - moved to base_data, + name: "Amsterdam Node".to_string(), + location: "Amsterdam, Netherlands".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 7, + memory_gb: 28, + storage_gb: 1200, + bandwidth_mbps: 100, + ssd_storage_gb: 600, + hdd_storage_gb: 600, + }, + uptime_percentage: 98.9, + earnings_today_usd: rust_decimal::Decimal::from(38), + last_seen: chrono::Utc::now(), + health_score: 97.2, + region: "EU".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 1, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-NA-002".to_string() - moved to base_data, + name: "Toronto Node".to_string(), + location: "Toronto, Canada".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 7, + memory_gb: 38, + storage_gb: 1500, + bandwidth_mbps: 100, + ssd_storage_gb: 750, + hdd_storage_gb: 750, + }, + uptime_percentage: 99.1, + earnings_today_usd: rust_decimal::Decimal::from(42), + last_seen: chrono::Utc::now(), + health_score: 98.8, + region: "NA".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 1, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + ], + earnings_history: vec![ + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 115, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 128, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 122, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 135, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 132, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 118, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 125, + source: "Node Operations".to_string(), + }, + ], + slice_templates: Vec::new(), + active_slices: 0, + }), + app_provider_data: None, + service_provider_data: None, + customer_service_data: None, + } + } + + /// App Provider user with published applications + pub fn app_provider() -> Self { + Self { + active_deployments: 15, + active_slices: 28, + current_cost: 680, + balance: 3200, + wallet_balance_usd: rust_decimal_macros::dec!(3200.0), + owned_product_ids: vec![ + "app_001".to_string(), + "app_002".to_string(), + "app_003".to_string(), + "app_004".to_string(), + ], + active_rentals: vec![ + "rental_007".to_string(), + "rental_008".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_007".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "provider1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "App Revenue".to_string() + }, + amount: rust_decimal_macros::dec!(150.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-05T16:30:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_008".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "provider1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "App Revenue".to_string() + }, + amount: rust_decimal_macros::dec!(125.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-04T16:30:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 72, + memory: 68, + storage: 85, + network: 55, + }, + usd_usage_trend: vec![450, 520, 580, 620, 650, 680], + user_activity: UserActivityStats { + deployments: vec![12, 14, 13, 15, 16, 15], + resource_reservations: vec![20, 22, 25, 26, 28, 28], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-05".to_string(), + action: "App Update".to_string(), + status: "Completed".to_string(), + details: "Updated AI Analytics Platform to v2.1.0".to_string(), + }, + RecentActivity { + date: "2025-05-04".to_string(), + action: "New Deployment".to_string(), + status: "Active".to_string(), + details: "Customer deployed Database Cluster Pro".to_string(), + }, + RecentActivity { + date: "2025-05-03".to_string(), + action: "Revenue Payment".to_string(), + status: "Completed".to_string(), + details: "Received $850 USD Credits from app deployments".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 0, + slices: 12, + apps: 6, + gateways: 2, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 0, + slices: 10, + apps: 5, + gateways: 1, + }, + RegionDeployments { + region: "Asia".to_string(), + nodes: 0, + slices: 6, + apps: 4, + gateways: 1, + }, + ], + }, + farmer_data: None, + app_provider_data: Some(AppProviderData { + published_apps: 5, + total_deployments: 127, + active_deployments: 15, + monthly_revenue_usd: 2850, + total_revenue_usd: 42750, + apps: vec![ + PublishedApp::analytics_template("app-001", "AI Analytics Platform") + .with_stats(45, 4.8, 1200), + PublishedApp::database_template("app-002", "Database Cluster Pro") + .with_stats(32, 4.6, 850) + .with_last_updated("3 days ago"), + PublishedApp::web_template("app-003", "Web Server Stack") + .with_stats(28, 4.4, 520) + .with_last_updated("1 week ago"), + PublishedApp::builder() + .id("app-004") + .name("ML Training Suite") + .category("Machine Learning") + .version("1.2.0") + .deployments(15) + .rating(4.9) + .monthly_revenue_usd(180) + .last_updated("2 weeks ago") + .auto_healing(false) + .build() + .unwrap(), + PublishedApp::builder() + .id("app-005") + .name("Monitoring Dashboard") + .category("Monitoring") + .version("2.3.1") + .deployments(7) + .rating(4.2) + .monthly_revenue_usd(100) + .last_updated("1 month ago") + .auto_healing(true) + .build() + .unwrap(), + ], + deployment_stats: vec![ + DeploymentStat { + app_name: "AI Analytics Platform".to_string(), + region: "Europe".to_string(), + instances: 6, + status: "Running".to_string(), + resource_usage: ResourceUtilization { + cpu: 85, + memory: 78, + storage: 92, + network: 65, + }, + customer_name: None, + deployed_date: None, + deployment_base_data: BaseModelData::new(), + // id: None - moved to base_data, + auto_healing: Some(true), + }, + DeploymentStat { + app_name: "Database Cluster Pro".to_string(), + region: "North America".to_string(), + instances: 5, + status: "Running".to_string(), + resource_usage: ResourceUtilization { + cpu: 72, + memory: 88, + storage: 95, + network: 45, + }, + customer_name: None, + deployed_date: None, + deployment_base_data: BaseModelData::new(), + // id: None - moved to base_data, + auto_healing: Some(false), + }, + ], + revenue_history: vec![ + RevenueRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 95, + app_name: "AI Analytics Platform".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 88, + app_name: "Database Cluster Pro".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 72, + app_name: "Web Server Stack".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 105, + app_name: "AI Analytics Platform".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 82, + app_name: "Database Cluster Pro".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 67, + app_name: "ML Training Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 91, + app_name: "AI Analytics Platform".to_string(), + }, + ], + }), + service_provider_data: None, + customer_service_data: None, + } + } + + /// Service Provider user with active services + pub fn service_provider() -> Self { + Self { + active_deployments: 8, + active_slices: 16, + current_cost: 320, + balance: 4500, + wallet_balance_usd: rust_decimal_macros::dec!(4500.0), + owned_product_ids: vec![ + "service_001".to_string(), + "service_002".to_string(), + "service_003".to_string(), + ], + active_rentals: vec![ + "rental_009".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_009".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "service1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "Service Revenue".to_string() + }, + amount: rust_decimal_macros::dec!(200.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-05T18:45:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_010".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "service1@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Earning { + source: "Service Revenue".to_string() + }, + amount: rust_decimal_macros::dec!(180.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-04T18:45:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 58, + memory: 62, + storage: 45, + network: 40, + }, + usd_usage_trend: vec![250, 280, 300, 310, 315, 320], + user_activity: UserActivityStats { + deployments: vec![6, 7, 8, 8, 8, 8], + resource_reservations: vec![12, 14, 15, 16, 16, 16], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-05".to_string(), + action: "Service Completed".to_string(), + status: "Completed".to_string(), + details: "Completed DevOps consultation for TechCorp".to_string(), + }, + RecentActivity { + date: "2025-05-04".to_string(), + action: "New Client".to_string(), + status: "Active".to_string(), + details: "Started blockchain development for CryptoStart".to_string(), + }, + RecentActivity { + date: "2025-05-02".to_string(), + action: "Payment Received".to_string(), + status: "Completed".to_string(), + details: "Received $1200 USD Credits for cloud migration project".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 0, + slices: 8, + apps: 4, + gateways: 1, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 0, + slices: 8, + apps: 4, + gateways: 1, + }, + ], + }, + farmer_data: None, + app_provider_data: None, + service_provider_data: Some(ServiceProviderData { + active_services: 0, + total_clients: 0, + monthly_revenue_usd: 0, + total_revenue_usd: 0, + service_rating: 4.5, + services: Vec::new(), // Services will be loaded from persistent storage + client_requests: vec![ + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-001".to_string() - moved to base_data, + client_name: "TechCorp Inc.".to_string(), + service_name: "DevOps Consultation".to_string(), + status: "In Progress".to_string(), + requested_date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + estimated_hours: 40, + budget: 3400, + priority: "High".to_string(), + progress: Some(80), // In Progress requests have some progress + completed_date: None, + client_email: Some("contact@techcorp.com".to_string()), + client_phone: Some("+1-555-0100".to_string()), + description: Some("DevOps consultation for infrastructure optimization and CI/CD pipeline setup.".to_string()), + created_date: Some((Utc::now() - chrono::Duration::days(7)).format("%Y-%m-%d").to_string()), + }, + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-002".to_string() - moved to base_data, + client_name: "CryptoStart".to_string(), + service_name: "Blockchain Development".to_string(), + status: "Active".to_string(), + requested_date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + estimated_hours: 80, + budget: 9600, + priority: "Medium".to_string(), + progress: Some(25), // Active requests have some progress + completed_date: None, + client_email: Some("dev@cryptostart.io".to_string()), + client_phone: Some("+1-555-0300".to_string()), + description: Some("Custom blockchain development including smart contracts, DeFi protocols, and security auditing.".to_string()), + created_date: Some((Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string()), + }, + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-003".to_string() - moved to base_data, + client_name: "SecureBank".to_string(), + service_name: "Security Audit".to_string(), + status: "Pending".to_string(), + requested_date: Utc::now().format("%Y-%m-%d").to_string(), + estimated_hours: 25, + budget: 2750, + priority: "High".to_string(), + progress: None, // Pending requests have no progress yet + completed_date: None, + client_email: Some("security@securebank.com".to_string()), + client_phone: Some("+1-555-0400".to_string()), + description: Some("Comprehensive security audit of banking systems including penetration testing and compliance review.".to_string()), + created_date: Some((Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string()), + }, + ], + revenue_history: vec![ + RevenueRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 680, + app_name: "DevOps Consultation".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 950, + app_name: "Blockchain Development".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 1200, + app_name: "Cloud Migration".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 770, + app_name: "Security Audit".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 595, + app_name: "DevOps Consultation".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 1140, + app_name: "Cloud Migration".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 850, + app_name: "Blockchain Development".to_string(), + }, + ], + }), + customer_service_data: None, + } + } + + /// Power user with comprehensive data across all roles + pub fn user3() -> Self { + Self { + active_deployments: 25, + active_slices: 45, + current_cost: 1250, + balance: 8500, + wallet_balance_usd: rust_decimal_macros::dec!(8500.0), + owned_product_ids: vec![ + "compute_006".to_string(), + "compute_007".to_string(), + "hardware_004".to_string(), + "app_005".to_string(), + "service_004".to_string(), + ], + active_rentals: vec![ + "rental_010".to_string(), + "rental_011".to_string(), + "rental_012".to_string(), + ], + transaction_history: vec![ + Transaction { + base_data: BaseModelData::new(), + // id: "tx_011".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user3@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "hardware_004".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(3000.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-04T11:30:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + }, + Transaction { + base_data: BaseModelData::new(), + // id: "tx_012".to_string() - moved to base_data, + user_base_data: BaseModelData::new(), + // id: "user3@example.com".to_string() - moved to base_data, + transaction_type: TransactionType::Purchase { + product_base_data: BaseModelData::new(), + // id: "bank_transfer".to_string() - moved to base_data + }, + amount: rust_decimal_macros::dec!(5000.0), + timestamp: DateTime::parse_from_rfc3339("2025-05-01T08:45:00Z").unwrap().with_timezone(&Utc), + status: TransactionStatus::Completed, + } + ], + resource_utilization: ResourceUtilization { + cpu: 88, + memory: 82, + storage: 95, + network: 75, + }, + usd_usage_trend: vec![800, 950, 1100, 1180, 1220, 1250], + user_activity: UserActivityStats { + deployments: vec![20, 22, 24, 25, 25, 25], + resource_reservations: vec![35, 38, 42, 44, 45, 45], + }, + recent_activities: vec![ + RecentActivity { + date: "2025-05-06".to_string(), + action: "Multi-Role Activity".to_string(), + status: "Active".to_string(), + details: "Managing 5 nodes, 8 apps, and 3 service contracts".to_string(), + }, + RecentActivity { + date: "2025-05-05".to_string(), + action: "Node Expansion".to_string(), + status: "Completed".to_string(), + details: "Added new 3Node in Asia region".to_string(), + }, + RecentActivity { + date: "2025-05-04".to_string(), + action: "App Launch".to_string(), + status: "Completed".to_string(), + details: "Published new IoT Management Platform".to_string(), + }, + ], + deployment_distribution: DeploymentDistribution { + regions: vec![ + RegionDeployments { + region: "Europe".to_string(), + nodes: 2, + slices: 18, + apps: 10, + gateways: 3, + }, + RegionDeployments { + region: "North America".to_string(), + nodes: 2, + slices: 15, + apps: 8, + gateways: 2, + }, + RegionDeployments { + region: "Asia".to_string(), + nodes: 1, + slices: 12, + apps: 7, + gateways: 2, + }, + ], + }, + // Optimized farmer data - reduced for session storage + farmer_data: Some(FarmerData { + total_nodes: 5, + online_nodes: 5, + total_capacity: NodeCapacity { + cpu_cores: 80, + memory_gb: 320, + storage_gb: 10000, + bandwidth_mbps: 2500, + ssd_storage_gb: 5000, + hdd_storage_gb: 5000, + }, + used_capacity: NodeCapacity { + cpu_cores: 70, + memory_gb: 264, + storage_gb: 9500, + bandwidth_mbps: 1875, + ssd_storage_gb: 4750, + hdd_storage_gb: 4750, + }, + monthly_earnings_usd: 2850, + total_earnings_usd: 85500, + uptime_percentage: 99.8, + // Reduced to 2 nodes for session storage optimization + nodes: vec![ + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-EU-101".to_string() - moved to base_data, + name: "Frankfurt Powerhouse".to_string(), + location: "Frankfurt, Germany".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 15, + memory_gb: 58, + storage_gb: 1900, + bandwidth_mbps: 450, + ssd_storage_gb: 950, + hdd_storage_gb: 950, + }, + uptime_percentage: 99.9, + earnings_today_usd: rust_decimal::Decimal::from(95), + last_seen: chrono::Utc::now(), + health_score: 99.5, + region: "EU".to_string(), + node_type: "3Node".to_string(), + slice_formats: Some(vec!["basic".to_string(), "standard".to_string(), "performance".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 3, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + FarmNode { + base_data: BaseModelData::new(), + // id: "TF-AS-101".to_string() - moved to base_data, + name: "Singapore Gateway".to_string(), + location: "Singapore".to_string(), + status: crate::models::user::NodeStatus::Online, + capacity: NodeCapacity { + cpu_cores: 16, + memory_gb: 64, + storage_gb: 2000, + bandwidth_mbps: 500, + ssd_storage_gb: 1000, + hdd_storage_gb: 1000, + }, + used_capacity: NodeCapacity { + cpu_cores: 14, + memory_gb: 50, + storage_gb: 1200, + bandwidth_mbps: 300, + ssd_storage_gb: 600, + hdd_storage_gb: 600, + }, + uptime_percentage: 99.9, + earnings_today_usd: rust_decimal::Decimal::from(78), + last_seen: chrono::Utc::now(), + health_score: 99.2, + region: "AS".to_string(), + node_type: "Gateway".to_string(), + slice_formats: Some(vec!["standard".to_string(), "performance".to_string()]), + rental_options: None, + staking_options: None, + availability_status: NodeAvailabilityStatus::Available, + grid_node_base_data: BaseModelData::new(), + // id: None - moved to base_data, + grid_data: None, + node_group_base_data: BaseModelData::new(), + // id: None - moved to base_data, + group_assignment_date: None, + group_slice_format: None, + group_slice_price: None, + + // NEW: Marketplace SLA field (None for mock data) + marketplace_sla: None, + + total_base_slices: 4, + allocated_base_slices: 2, + slice_allocations: Vec::new(), + available_combinations: Vec::new(), + slice_pricing: crate::services::slice_calculator::SlicePricing::default(), + slice_last_calculated: Some(chrono::Utc::now()), + }, + ], + // Earnings history for the past 7 days + earnings_history: vec![ + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 420, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 445, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 432, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 458, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 441, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 467, + source: "Node Operations".to_string(), + }, + EarningsRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 438, + source: "Node Operations".to_string(), + }, + ], + slice_templates: Vec::new(), + active_slices: 0, + }), + // Optimized app provider data - reduced for session storage + app_provider_data: Some(AppProviderData { + published_apps: 8, + total_deployments: 285, + active_deployments: 25, + monthly_revenue_usd: 5200, + total_revenue_usd: 156000, + // Reduced to 1 app for session storage optimization + apps: vec![ + PublishedApp::builder() + .id("app-101") + .name("Enterprise AI Suite") + .category("AI/ML") + .version("3.2.1") + .deployments(85) + .rating(4.9) + .monthly_revenue_usd(2100) + .last_updated("Today") + .auto_healing(true) + .build() + .unwrap(), + ], + // Reduced deployment stats + deployment_stats: vec![ + DeploymentStat::builder() + .app_name("Enterprise AI Suite") + .region("Global") + .instances(25) + .status("Running") + .resource_usage(ResourceUtilization { + cpu: 92, + memory: 88, + storage: 95, + network: 78, + }) + .auto_healing(true) + .build() + .unwrap(), + ], + // Enhanced revenue history for 7 days + revenue_history: vec![ + RevenueRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 285, + app_name: "Enterprise AI Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 320, + app_name: "Enterprise AI Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 275, + app_name: "Enterprise AI Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 310, + app_name: "Enterprise AI Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 295, + app_name: "Enterprise AI Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 340, + app_name: "Enterprise AI Suite".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 265, + app_name: "Enterprise AI Suite".to_string(), + }, + ], + }), + // Optimized service provider data - reduced for session storage + service_provider_data: Some(ServiceProviderData { + active_services: 0, + total_clients: 0, + monthly_revenue_usd: 0, + total_revenue_usd: 0, + service_rating: 4.5, + services: Vec::new(), // Services will be loaded from persistent storage + // Reduced client requests + client_requests: vec![ + ServiceRequest { + base_data: BaseModelData::new(), + // id: "req-101".to_string() - moved to base_data, + client_name: "Global Corp".to_string(), + service_name: "Enterprise Architecture".to_string(), + status: "Active".to_string(), + requested_date: Utc::now().format("%Y-%m-%d").to_string(), + estimated_hours: 120, + budget: 18000, + priority: "High".to_string(), + progress: Some(15), // Active requests have some progress + completed_date: None, + client_email: Some("enterprise@globalcorp.com".to_string()), + client_phone: Some("+1-555-0500".to_string()), + description: Some("Enterprise architecture design and implementation for large-scale digital transformation project.".to_string()), + created_date: Some((Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string()), + }, + ], + // Enhanced revenue history for 7 days + revenue_history: vec![ + RevenueRecord { + date: Utc::now().format("%Y-%m-%d").to_string(), + amount: 1200, + app_name: "Enterprise Architecture".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(1)).format("%Y-%m-%d").to_string(), + amount: 1450, + app_name: "Enterprise Architecture".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(2)).format("%Y-%m-%d").to_string(), + amount: 1100, + app_name: "Enterprise Architecture".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(3)).format("%Y-%m-%d").to_string(), + amount: 1350, + app_name: "Enterprise Architecture".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(4)).format("%Y-%m-%d").to_string(), + amount: 1275, + app_name: "Enterprise Architecture".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(5)).format("%Y-%m-%d").to_string(), + amount: 1520, + app_name: "Enterprise Architecture".to_string(), + }, + RevenueRecord { + date: (Utc::now() - chrono::Duration::days(6)).format("%Y-%m-%d").to_string(), + amount: 1180, + app_name: "Enterprise Architecture".to_string(), + }, + ], + }), + customer_service_data: None, + } + } +} + +impl User { + /// Creates a new user with mock data + pub fn new_with_mock_data(name: String, email: String, mock_data: MockUserData) -> Self { + let mut user = Self::new(name, email); + user.mock_data = Some(mock_data); + user + } + + /// Get user's wallet balance + pub fn get_wallet_balance(&self) -> rust_decimal::Decimal { + self.mock_data.as_ref() + .map(|data| data.wallet_balance_usd) + .unwrap_or(rust_decimal_macros::dec!(0)) + } + + /// Get user's owned products + pub fn get_owned_products(&self) -> Vec { + self.mock_data.as_ref() + .map(|data| data.owned_product_ids.clone()) + .unwrap_or_default() + } + + /// Get user's active rentals + pub fn get_active_rentals(&self) -> Vec { + self.mock_data.as_ref() + .map(|data| data.active_rentals.clone()) + .unwrap_or_default() + } + + /// Get user's transaction history + pub fn get_transaction_history(&self) -> Vec { + self.mock_data.as_ref() + .map(|data| data.transaction_history.clone()) + .unwrap_or_default() + } +} + +/// User deployment information for dashboard +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserDeployment { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub app_name: String, + pub status: DeploymentStatus, + pub cost_per_month: Decimal, + #[serde(deserialize_with = "deserialize_datetime")] + pub deployed_at: DateTime, + pub provider: String, + pub region: String, + pub resource_usage: ResourceUtilization, +} + +/// Deployment status enum +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DeploymentStatus { + Active, + Pending, + Stopped, + Error, + Maintenance, +} + +impl Default for DeploymentStatus { + fn default() -> Self { + Self::Pending + } +} + +/// Comprehensive user metrics for dashboard +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserMetrics { + pub total_spent_this_month: Decimal, + pub active_deployments_count: i32, + pub resource_utilization: ResourceUtilization, + pub cost_trend: Vec, + pub wallet_balance: Decimal, + pub total_transactions: i32, +} + +impl Default for UserMetrics { + fn default() -> Self { + Self { + total_spent_this_month: Decimal::ZERO, + active_deployments_count: 0, + resource_utilization: ResourceUtilization { + cpu: 0, + memory: 0, + storage: 0, + network: 0, + }, + cost_trend: vec![0; 6], + wallet_balance: Decimal::ZERO, + total_transactions: 0, + } + } +} + +/// User compute resource for dashboard display +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserComputeResource { + + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub resource_type: String, // "Compute", "Storage", "AI/GPU" + pub specs: String, + pub location: String, + pub status: String, + pub sla: String, + pub monthly_cost: Decimal, + pub provider: String, + pub resource_usage: ResourceUtilization, +} + + +#[derive(Default)] +pub struct UserBuilder { + base_data: BaseModelData::new(), + // id: Option - moved to base_data, + name: Option, + email: Option, + role: Option, + country: Option, + timezone: Option, + // created_at: Option> - moved to base_data, + // updated_at: Option> - moved to base_data, + mock_data: Option, +} + +impl UserBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn id(mut self, id: i32) -> Self { + self.base_data.id = Some(id.to_string()); + self + } + + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn email(mut self, email: impl Into) -> Self { + self.email = Some(email.into()); + self + } + + pub fn role(mut self, role: UserRole) -> Self { + self.role = Some(role); + self + } + + pub fn country(mut self, country: impl Into) -> Self { + self.country = Some(country.into()); + self + } + + pub fn timezone(mut self, timezone: impl Into) -> Self { + self.timezone = Some(timezone.into()); + self + } + + pub fn mock_data(mut self, mock_data: MockUserData) -> Self { + self.mock_data = Some(mock_data); + self + } + + pub fn build(self) -> Result { + let now = Utc::now(); + Ok(User { + base_data: BaseModelData::new(), + // id: self.base_data.id - moved to base_data, + name: self.name.ok_or("name is required")?, + email: self.email.ok_or("email is required")?, + role: self.role.unwrap_or(UserRole::User), + country: self.country, + timezone: self.timezone, + // created_at: self.base_data.created_at.or(Some(now)) - moved to base_data, + // updated_at: self.base_data.updated_at.or(Some(now)) - moved to base_data, + mock_data: self.mock_data, + }) + } +} + +impl User { + pub fn builder() -> UserBuilder { + UserBuilder::new() + } + + // Template methods for common user types + pub fn new_user_template(name: impl Into, email: impl Into) -> Self { + Self::builder() + .name(name) + .email(email) + .role(UserRole::User) + .build() + .unwrap() + } + + pub fn admin_template(name: impl Into, email: impl Into) -> Self { + Self::builder() + .name(name) + .email(email) + .role(UserRole::Admin) + .build() + .unwrap() + } +} + + +// QUESTION: +/// User currency preferences +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserCurrencyPreference { + + /// Base model data (includes id, created_at, updated_at) + pub base_data: BaseModelData, + pub user_base_data: BaseModelData::new(), + // id: String - moved to base_data, + pub preferred_currency: String, + +} + +impl UserCurrencyPreference { + pub fn new(user_id: &str, name: &str) -> Self { + Self { + user_id, + preferred_currency, + // updated_at: Utc::now() - moved to base_data, + } + } + + pub fn update_preference(&mut self, new_currency: String) { + self.preferred_currency = new_currency; + self.base_data.updated_at = Utc::now(); + } +} \ No newline at end of file