1 Commits

Author SHA1 Message Date
Timur Gordon
6569e819ae marketplace models wip 2025-08-21 14:07:14 +02:00
16 changed files with 7987 additions and 0 deletions

View File

@@ -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<String> - moved to base_data,
activity_type: Option<crate::models::user::ActivityType>,
description: Option<String>,
timestamp: Option<chrono::DateTime<chrono::Utc>>,
metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
category: Option<String>,
importance: Option<crate::models::user::ActivityImportance>,
}
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<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn timestamp(mut self, timestamp: chrono::DateTime<chrono::Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
pub fn metadata(mut self, metadata: std::collections::HashMap<String, serde_json::Value>) -> Self {
self.metadata = Some(metadata);
self
}
pub fn category(mut self, category: impl Into<String>) -> 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<crate::models::user::UserActivity, String> {
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<Utc>,
pub metadata: std::collections::HashMap<String, serde_json::Value>,
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,
}

View File

@@ -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<String>,
pub version: Option<String>,
pub status: String,
// Deployment information
pub customer_name: Option<String>,
pub customer_email: Option<String>,
pub deployed_date: Option<String>,
pub health_score: Option<f32>,
pub region: Option<String>,
pub instances: Option<i32>,
pub resource_usage: Option<ResourceUtilization>,
// Business metrics
pub deployments: Option<i32>,
pub rating: Option<f32>,
pub monthly_revenue_usd: Option<i32>,
pub cost_per_month: Option<Decimal>,
// Metadata
pub last_updated: Option<String>,
pub auto_healing: Option<bool>,
pub provider: Option<String>,
pub deployed_at: Option<DateTime<Utc>>,
}
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<f32>,
pub resource_usage: Option<ResourceUtilization>,
pub deployed_at: Option<DateTime<Utc>>,
}
/// 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<String>,
category: Option<String>,
version: Option<String>,
status: Option<String>,
customer_name: Option<String>,
customer_email: Option<String>,
deployed_date: Option<String>,
health_score: Option<f32>,
region: Option<String>,
instances: Option<i32>,
resource_usage: Option<ResourceUtilization>,
deployments: Option<i32>,
rating: Option<f32>,
monthly_revenue_usd: Option<i32>,
cost_per_month: Option<Decimal>,
last_updated: Option<String>,
auto_healing: Option<bool>,
provider: Option<String>,
deployed_at: Option<DateTime<Utc>>,
}
impl AppBuilder {
pub fn new() -> Self {
Self {
base_data: BaseModelData::new(),
..Default::default()
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn status(mut self, status: impl Into<String>) -> Self {
self.status = Some(status.into());
self
}
pub fn customer_name(mut self, name: impl Into<String>) -> Self {
self.customer_name = Some(name.into());
self
}
pub fn customer_email(mut self, email: impl Into<String>) -> Self {
self.customer_email = Some(email.into());
self
}
pub fn deployed_date(mut self, date: impl Into<String>) -> 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<String>) -> 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<String>) -> 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<String>) -> Self {
self.provider = Some(provider.into());
self
}
pub fn deployed_at(mut self, date: DateTime<Utc>) -> Self {
self.deployed_at = Some(date);
self
}
pub fn build(self) -> Result<App, String> {
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<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn with_last_updated(mut self, updated: impl Into<String>) -> 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;

View File

@@ -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<String>,
include_farmer_data: Option<bool>,
include_service_data: Option<bool>,
include_app_data: Option<bool>,
}
impl MockDataBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn user_type(mut self, user_type: impl Into<String>) -> 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<i32>,
online_nodes: Option<i32>,
total_capacity: Option<crate::models::user::NodeCapacity>,
used_capacity: Option<crate::models::user::NodeCapacity>,
monthly_earnings: Option<i32>,
total_earnings: Option<i32>,
uptime_percentage: Option<f32>,
nodes: Option<Vec<crate::models::user::FarmNode>>,
earnings_history: Option<Vec<crate::models::user::EarningsRecord>>,
active_slices: Option<i32>,
}
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<crate::models::user::FarmNode>) -> Self {
self.nodes = Some(nodes);
self
}
pub fn earnings_history(mut self, history: Vec<crate::models::user::EarningsRecord>) -> Self {
self.earnings_history = Some(history);
self
}
pub fn earnings(mut self, earnings: Vec<crate::models::user::EarningsRecord>) -> 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::<f32>() / 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::<i32>().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<crate::models::user::FarmerData, String> {
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<String>,
amount: Option<i32>,
service_name: Option<String>,
provider_name: Option<String>,
}
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<crate::models::user::SpendingRecord, String> {
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<bool>,
threshold_amount: Option<Decimal>,
topup_amount: Option<Decimal>,
payment_method_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
daily_limit: Option<Decimal>,
monthly_limit: Option<Decimal>,
}
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<crate::services::user_persistence::AutoTopUpSettings, String> {
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,
})
}
}

View File

@@ -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<String, serde_json::Value>,
pub added_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cart {
pub base_data: BaseModelData,
pub items: Vec<CartItem>,
}
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,
}
}
}

View File

@@ -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<Utc>,
}
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;
}
}

View File

@@ -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<FarmNode>,
pub earnings_history: Vec<EarningsRecord>,
pub slice_templates: Vec<crate::models::product::Product>,
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<String>,
#[serde(default)]
pub default_slice_customizations: Option<std::collections::HashMap<String, serde_json::Value>>, // Placeholder for DefaultSliceFormat
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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<OrderItem>,
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<PaymentDetails>,
pub billing_address: Option<Address>,
pub shipping_address: Option<Address>,
pub notes: Option<String>,
}
#[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<String, serde_json::Value>,
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<String> - moved to base_data,
user_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
items: Vec<OrderItem>,
subtotal_base: Option<Decimal>,
total_base: Option<Decimal>,
base_currency: Option<String>,
currency_used: Option<String>,
currency_total: Option<Decimal>,
conversion_rate: Option<Decimal>,
status: Option<OrderStatus>,
payment_method: Option<String>,
payment_details: Option<PaymentDetails>,
billing_address: Option<Address>,
shipping_address: Option<Address>,
notes: Option<String>,
purchase_type: Option<PurchaseType>,
// created_at: Option<DateTime<Utc>> - moved to base_data,
// updated_at: Option<DateTime<Utc>> - 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<OrderItem>) -> 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<String>) -> Self {
self.base_currency = Some(currency.into());
self
}
pub fn currency_used(mut self, currency: impl Into<String>) -> 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<String>) -> 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<String>) -> 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<Order, String> {
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<String> - moved to base_data,
product_name: Option<String>,
product_category: Option<String>,
quantity: Option<u32>,
unit_price_base: Option<Decimal>,
total_price_base: Option<Decimal>,
specifications: HashMap<String, Value>,
provider_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
provider_name: Option<String>,
}
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<String>) -> Self {
self.product_name = Some(name.into());
self
}
pub fn product_category(mut self, category: impl Into<String>) -> 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<String>, 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<String>) -> Self {
self.provider_name = Some(name.into());
self
}
pub fn build(self) -> Result<OrderItem, String> {
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()
}
}

View File

@@ -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<String> - moved to base_data,
pub payment_status: PaymentStatus,
pub payment_timestamp: Option<DateTime<Utc>>,
pub failure_reason: Option<String>,
}
#[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());
}
}

View File

@@ -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<Decimal>,
pub slippage_tolerance: Option<Decimal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeResponse {
pub success: bool,
pub message: String,
pub transaction_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
pub from_amount: Option<Decimal>,
pub to_amount: Option<Decimal>,
pub exchange_rate: Option<Decimal>,
pub fee: Option<Decimal>,
}
#[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<Utc>,
pub end_date: DateTime<Utc>,
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<PricePoint>,
pub volume_history: Vec<VolumePoint>,
pub liquidity_distribution: HashMap<String, Decimal>,
pub staking_distribution: HashMap<String, i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PricePoint {
pub timestamp: DateTime<Utc>,
pub price: Decimal,
pub volume: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumePoint {
pub date: String,
pub volume: Decimal,
}

View File

@@ -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<String, ProductAttribute>, // 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<AttributeDefinition>, // Defines allowed attributes
pub parent_category: Option<String>,
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<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AttributeType {
Text,
Number,
SliceConfiguration,
Boolean,
Select(Vec<String>), // Predefined options
MultiSelect(Vec<String>),
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<ValidationRule>,
}
#[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<String>,
pub location: Option<String>,
pub rating: Option<f32>,
pub review_count: u32,
pub featured: bool,
pub last_updated: chrono::DateTime<chrono::Utc>,
pub visibility: ProductVisibility,
pub seo_keywords: Vec<String>,
pub custom_fields: HashMap<String, serde_json::Value>,
}
/// 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<PriceTier>), // Volume discounts
Custom(String), // Marketplace-specific
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceTier {
pub min_quantity: u32,
pub max_quantity: Option<u32>,
pub price_per_unit: Decimal,
pub discount_percentage: Option<f32>,
}
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<SliceConfiguration> {
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<String> {
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<String> - moved to base_data,
name: Option<String>,
category_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
description: Option<String>,
base_price: Option<Decimal>,
base_currency: Option<String>,
attributes: HashMap<String, ProductAttribute>,
provider_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
provider_name: Option<String>,
availability: Option<ProductAvailability>,
metadata: Option<ProductMetadata>,
// created_at: Option<DateTime<Utc>> - moved to base_data,
// updated_at: Option<DateTime<Utc>> - moved to base_data,
}
impl ProductBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.base_data.id = Some(id.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn category_id(mut self, category_id: impl Into<String>) -> Self {
self.category_id = Some(category_id.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> 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<String>) -> Self {
self.base_currency = Some(currency.into());
self
}
pub fn add_attribute(mut self, key: impl Into<String>, attribute: ProductAttribute) -> Self {
self.attributes.insert(key.into(), attribute);
self
}
pub fn provider_id(mut self, provider_id: impl Into<String>) -> Self {
self.provider_id = Some(provider_id.into());
self
}
pub fn provider_name(mut self, provider_name: impl Into<String>) -> 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<Product, String> {
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()
}
}

View File

@@ -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<Service>,
pub client_requests: Vec<ServiceRequest>,
pub revenue_history: Vec<RevenueRecord>,
}
#[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<i32>,
#[serde(default)]
pub completed_date: Option<String>,
#[serde(default)]
pub client_email: Option<String>,
#[serde(default)]
pub client_phone: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub created_date: Option<String>,
}
/// 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<String>,
pub booking_date: String, // When customer booked
pub client_phone: Option<String>,
pub progress: Option<i32>,
pub completed_date: Option<String>,
}
/// 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<ServiceBooking>,
pub favorite_providers: Vec<String>,
pub spending_history: Vec<SpendingRecord>,
}
#[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<String> - moved to base_data,
service_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
service_name: Option<String>,
provider_email: Option<String>,
customer_email: Option<String>,
budget: Option<i32>,
estimated_hours: Option<i32>,
status: Option<String>,
requested_date: Option<String>,
priority: Option<String>,
description: Option<String>,
booking_date: Option<String>,
}
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<String>) -> 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<ServiceBooking, String> {
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<i32>,
completed_bookings: Option<i32>,
total_spent: Option<i32>,
monthly_spending: Option<i32>,
average_rating_given: Option<f32>,
service_bookings: Option<Vec<crate::models::user::ServiceBooking>>,
favorite_providers: Option<Vec<String>>,
spending_history: Option<Vec<crate::models::user::SpendingRecord>>,
}
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<crate::models::user::ServiceBooking>) -> Self {
self.service_bookings = Some(bookings);
self
}
pub fn favorite_providers(mut self, providers: Vec<String>) -> Self {
self.favorite_providers = Some(providers);
self
}
pub fn spending_history(mut self, history: Vec<crate::models::user::SpendingRecord>) -> Self {
self.spending_history = Some(history);
self
}
pub fn build(self) -> Result<crate::models::user::CustomerServiceData, String> {
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()
}
}

View File

@@ -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<String> - 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<String> - moved to base_data,
farmer_name: Option<String>,
slice_name: Option<String>,
cpu_cores: Option<i32>,
memory_gb: Option<i32>,
storage_gb: Option<i32>,
bandwidth_mbps: Option<i32>,
min_uptime_sla: Option<f32>,
public_ips: Option<i32>,
node_base_data: BaseModelData::new(),
// id: Option<String> - moved to base_data,
slice_type: Option<crate::models::tfmarketplace::product::SliceType>,
price_per_hour: Option<rust_decimal::Decimal>,
}
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<String>) -> Self {
self.farmer_name = Some(farmer_name.into());
self
}
pub fn slice_name(mut self, slice_name: impl Into<String>) -> 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<crate::models::tfmarketplace::product::Product, String> {
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,
))
}
}

File diff suppressed because it is too large Load Diff