Compare commits
	
		
			2 Commits
		
	
	
		
			developmen
			...
			7a999b7b6e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7a999b7b6e | |||
| 095a4d0c69 | 
							
								
								
									
										48
									
								
								heromodels/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										48
									
								
								heromodels/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -60,7 +60,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -233,6 +233,14 @@ dependencies = [ | ||||
|  "typenum", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "derive" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "quote", | ||||
|  "syn 1.0.109", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "digest" | ||||
| version = "0.10.7" | ||||
| @@ -292,7 +300,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -379,6 +387,7 @@ version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "bincode", | ||||
|  "chrono", | ||||
|  "derive", | ||||
|  "heromodels-derive", | ||||
|  "heromodels_core", | ||||
|  "jsonb", | ||||
| @@ -402,7 +411,7 @@ version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -505,7 +514,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -943,7 +952,7 @@ checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1006,7 +1015,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1136,7 +1145,7 @@ dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "rustversion", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1145,6 +1154,17 @@ version = "2.6.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | ||||
|  | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "1.0.109" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "unicode-ident", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "2.0.104" | ||||
| @@ -1179,7 +1199,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1234,7 +1254,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1389,7 +1409,7 @@ dependencies = [ | ||||
|  "log", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
|  "wasm-bindgen-shared", | ||||
| ] | ||||
|  | ||||
| @@ -1411,7 +1431,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
|  "wasm-bindgen-backend", | ||||
|  "wasm-bindgen-shared", | ||||
| ] | ||||
| @@ -1467,7 +1487,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1478,7 +1498,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1613,5 +1633,5 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "syn 2.0.104", | ||||
| ] | ||||
|   | ||||
| @@ -1,115 +0,0 @@ | ||||
| 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, | ||||
| } | ||||
| @@ -1,361 +0,0 @@ | ||||
| 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; | ||||
| @@ -1,351 +0,0 @@ | ||||
| //! 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, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @@ -1,105 +0,0 @@ | ||||
| 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, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,90 +0,0 @@ | ||||
| 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; | ||||
|     } | ||||
| } | ||||
| @@ -1,30 +0,0 @@ | ||||
|  | ||||
| /// 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 | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| // 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
											
										
									
								
							| @@ -1,8 +0,0 @@ | ||||
| # 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 | ||||
| @@ -1,402 +0,0 @@ | ||||
| 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() | ||||
|     } | ||||
| } | ||||
| @@ -1,77 +0,0 @@ | ||||
| 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()); | ||||
|     } | ||||
| } | ||||
| @@ -1,105 +0,0 @@ | ||||
| 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, | ||||
| } | ||||
| @@ -1,660 +0,0 @@ | ||||
| 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() | ||||
|     } | ||||
| } | ||||
| @@ -1,297 +0,0 @@ | ||||
| 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() | ||||
|     } | ||||
| } | ||||
| @@ -1,200 +0,0 @@ | ||||
| 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
											
										
									
								
							
							
								
								
									
										345
									
								
								specs/billingmanager_research/billingmanager.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								specs/billingmanager_research/billingmanager.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,345 @@ | ||||
|  | ||||
| ### 2.1 Accounts | ||||
|  | ||||
| * **id**: `BIGINT` identity (non-negative), unique account id | ||||
| * **pubkey**: `BYTEA` unique public key for signing/encryption | ||||
| * **display\_name**: `TEXT` (optional) | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| ### 2.2 Currencies | ||||
|  | ||||
| * **asset\_code**: `TEXT` PK (e.g., `USDC-ETH`, `EUR`, `LND`) | ||||
| * **name**: `TEXT` | ||||
| * **symbol**: `TEXT` | ||||
| * **decimals**: `INT` (default 2) | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 3) Services & Groups | ||||
|  | ||||
| ### 3.1 Services | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **name**: `TEXT` unique | ||||
| * **description**: `TEXT` | ||||
| * **default\_billing\_mode**: `ENUM('per_second','per_request')` | ||||
| * **default\_price**: `NUMERIC(38,18)` (≥0) | ||||
| * **default\_currency**: FK → `currencies(asset_code)` | ||||
| * **max\_request\_seconds**: `INT` (>0 or `NULL`) | ||||
| * **schema\_heroscript**: `TEXT` | ||||
| * **schema\_json**: `JSONB` | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| #### Accepted Currencies (per service) | ||||
|  | ||||
| * **service\_id**: FK → `services(id)` | ||||
| * **asset\_code**: FK → `currencies(asset_code)` | ||||
| * **price\_override**: `NUMERIC(38,18)` (optional) | ||||
| * **billing\_mode\_override**: `ENUM` (optional) | ||||
|   Primary key: `(service_id, asset_code)` | ||||
|  | ||||
| ### 3.2 Service Groups | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **name**: `TEXT` unique | ||||
| * **description**: `TEXT` | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| #### Group Memberships | ||||
|  | ||||
| * **group\_id**: FK → `service_groups(id)` | ||||
| * **service\_id**: FK → `services(id)` | ||||
|   Primary key: `(group_id, service_id)` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 4) Providers & Runners | ||||
|  | ||||
| ### 4.1 Service Providers | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **account\_id**: FK → `accounts(id)` (the owning account) | ||||
| * **name**: `TEXT` unique | ||||
| * **description**: `TEXT` | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| #### Providers Offer Groups | ||||
|  | ||||
| * **provider\_id**: FK → `service_providers(id)` | ||||
| * **group\_id**: FK → `service_groups(id)` | ||||
|   Primary key: `(provider_id, group_id)` | ||||
|  | ||||
| #### Provider Pricing Overrides (optional) | ||||
|  | ||||
| * **provider\_id**: FK → `service_providers(id)` | ||||
| * **service\_id**: FK → `services(id)` | ||||
| * **asset\_code**: FK → `currencies(asset_code)` (nullable for currency-agnostic override) | ||||
| * **price\_override**: `NUMERIC(38,18)` (optional) | ||||
| * **billing\_mode\_override**: `ENUM` (optional) | ||||
| * **max\_request\_seconds\_override**: `INT` (optional) | ||||
|   Primary key: `(provider_id, service_id, asset_code)` | ||||
|  | ||||
| ### 4.2 Runners | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **address**: `INET` (must be IPv6) | ||||
| * **name**: `TEXT` | ||||
| * **description**: `TEXT` | ||||
| * **pubkey**: `BYTEA` (optional) | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| #### Runner Ownership (many-to-many) | ||||
|  | ||||
| * **runner\_id**: FK → `runners(id)` | ||||
| * **provider\_id**: FK → `service_providers(id)` | ||||
|   Primary key: `(runner_id, provider_id)` | ||||
|  | ||||
| #### Routing (provider → service/service\_group → runners) | ||||
|  | ||||
| * **provider\_service\_runners**: `(provider_id, service_id, runner_id)` PK | ||||
| * **provider\_service\_group\_runners**: `(provider_id, group_id, runner_id)` PK | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 5) Subscriptions & Spend Control | ||||
|  | ||||
| A subscription authorizes an **account** to use either a **service** **or** a **service group**, with optional spend limits and allowed providers. | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **account\_id**: FK → `accounts(id)` | ||||
| * **service\_id** *xor* **group\_id**: FK (exactly one must be set) | ||||
| * **secret**: `BYTEA` (random, provided by subscriber; recommend storing a hash) | ||||
| * **subscription\_data**: `JSONB` (free-form) | ||||
| * **limit\_amount**: `NUMERIC(38,18)` (optional) | ||||
| * **limit\_currency**: FK → `currencies(asset_code)` (optional) | ||||
| * **limit\_period**: `ENUM('hour','day','month')` (optional) | ||||
| * **active**: `BOOLEAN` default `TRUE` | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| #### Allowed Providers per Subscription | ||||
|  | ||||
| * **subscription\_id**: FK → `subscriptions(id)` | ||||
| * **provider\_id**: FK → `service_providers(id)` | ||||
|   Primary key: `(subscription_id, provider_id)` | ||||
|  | ||||
| **Intended Use:** | ||||
|  | ||||
| * Subscribers bound spending by amount/currency/period. | ||||
| * Merchant (provider) can claim charges for requests fulfilled under an active subscription, within limits, and only if listed in `subscription_providers`. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 6) Requests & Billing | ||||
|  | ||||
| ### 6.1 Request Lifecycle | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **account\_id**: FK → `accounts(id)` | ||||
| * **subscription\_id**: FK → `subscriptions(id)` | ||||
| * **provider\_id**: FK → `service_providers(id)` | ||||
| * **service\_id**: FK → `services(id)` | ||||
| * **runner\_id**: FK → `runners(id)` (nullable) | ||||
| * **request\_schema**: `JSONB` (payload matching `schema_json`/`schema_heroscript`) | ||||
| * **started\_at**, **ended\_at**: `TIMESTAMPTZ` | ||||
| * **status**: `ENUM('pending','running','succeeded','failed','canceled')` | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| ### 6.2 Billing Ledger (append-only) | ||||
|  | ||||
| * **id**: `BIGINT` identity | ||||
| * **account\_id**: FK → `accounts(id)` | ||||
| * **provider\_id**: FK → `service_providers(id)` (nullable) | ||||
| * **service\_id**: FK → `services(id)` (nullable) | ||||
| * **request\_id**: FK → `requests(id)` (nullable) | ||||
| * **amount**: `NUMERIC(38,18)` (debit = positive, credit/refund = negative) | ||||
| * **asset\_code**: FK → `currencies(asset_code)` | ||||
| * **entry\_type**: `ENUM('debit','credit','adjustment')` | ||||
| * **description**: `TEXT` | ||||
| * **created\_at**: `TIMESTAMPTZ` | ||||
|  | ||||
| **Balances View (example):** | ||||
|  | ||||
| * `account_balances(account_id, asset_code, balance)` as a view over `billing_ledger`. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 7) Pricing Precedence | ||||
|  | ||||
| When computing the **effective** pricing, billing mode, and max duration for a `(provider, service, currency)`: | ||||
|  | ||||
| 1. **Provider override for (service, asset\_code)** — if present, use it. | ||||
| 2. **Service accepted currency override** — if present, use it. | ||||
| 3. **Service defaults** — fallback. | ||||
|  | ||||
| If `billing_mode` or `max_request_seconds` are not overridden at steps (1) or (2), inherit from the next step down. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 8) Key Constraints & Validations | ||||
|  | ||||
| * All identity ids are non-negative (`CHECK (id >= 0)`). | ||||
| * Runner IPv6 enforcement: `CHECK (family(address) = 6)`. | ||||
| * Subscriptions must point to **exactly one** of `service_id` or `group_id`. | ||||
| * Prices and limits must be non-negative if set. | ||||
| * Unique natural keys where appropriate: service names, provider names, currency asset codes, account pubkeys. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 9) Mermaid Diagrams | ||||
|  | ||||
| ### 9.1 Entity–Relationship Overview | ||||
|  | ||||
| ```mermaid | ||||
| erDiagram | ||||
|     ACCOUNTS ||--o{ SERVICE_PROVIDERS : "owns via account_id" | ||||
|     ACCOUNTS ||--o{ SUBSCRIPTIONS : has | ||||
|     CURRENCIES ||--o{ SERVICES : "default_currency" | ||||
|     CURRENCIES ||--o{ SERVICE_ACCEPTED_CURRENCIES : "asset_code" | ||||
|     CURRENCIES ||--o{ PROVIDER_SERVICE_OVERRIDES : "asset_code" | ||||
|     CURRENCIES ||--o{ BILLING_LEDGER : "asset_code" | ||||
|  | ||||
|     SERVICES ||--o{ SERVICE_ACCEPTED_CURRENCIES : has | ||||
|     SERVICES ||--o{ SERVICE_GROUP_MEMBERS : member_of | ||||
|     SERVICE_GROUPS ||--o{ SERVICE_GROUP_MEMBERS : contains | ||||
|  | ||||
|     SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_GROUPS : offers | ||||
|     SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_OVERRIDES : sets | ||||
|     SERVICE_PROVIDERS ||--o{ RUNNER_OWNERS : owns | ||||
|     SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_RUNNERS : routes | ||||
|     SERVICE_PROVIDERS ||--o{ PROVIDER_SERVICE_GROUP_RUNNERS : routes | ||||
|  | ||||
|     RUNNERS ||--o{ RUNNER_OWNERS : owned_by | ||||
|     RUNNERS ||--o{ PROVIDER_SERVICE_RUNNERS : executes | ||||
|     RUNNERS ||--o{ PROVIDER_SERVICE_GROUP_RUNNERS : executes | ||||
|  | ||||
|     SUBSCRIPTIONS ||--o{ SUBSCRIPTION_PROVIDERS : allow | ||||
|     SERVICE_PROVIDERS ||--o{ SUBSCRIPTION_PROVIDERS : allowed | ||||
|  | ||||
|     REQUESTS }o--|| ACCOUNTS : by | ||||
|     REQUESTS }o--|| SUBSCRIPTIONS : under | ||||
|     REQUESTS }o--|| SERVICE_PROVIDERS : via | ||||
|     REQUESTS }o--|| SERVICES : for | ||||
|     REQUESTS }o--o{ RUNNERS : executed_by | ||||
|  | ||||
|     BILLING_LEDGER }o--|| ACCOUNTS : charges | ||||
|     BILLING_LEDGER }o--o{ SERVICES : reference | ||||
|     BILLING_LEDGER }o--o{ SERVICE_PROVIDERS : reference | ||||
|     BILLING_LEDGER }o--o{ REQUESTS : reference | ||||
| ``` | ||||
|  | ||||
| ### 9.2 Request Flow (Happy Path) | ||||
|  | ||||
| ```mermaid | ||||
| sequenceDiagram | ||||
|     autonumber | ||||
|     participant AC as Account | ||||
|     participant API as Broker/API | ||||
|     participant PR as Provider | ||||
|     participant RU as Runner | ||||
|     participant DB as PostgreSQL | ||||
|  | ||||
|     AC->>API: Submit request (subscription_id, service_id, payload, secret) | ||||
|     API->>DB: Validate subscription (active, provider allowed, spend limits) | ||||
|     DB-->>API: OK + effective pricing (resolve precedence) | ||||
|     API->>PR: Dispatch request (service, payload) | ||||
|     PR->>DB: Select runner (provider_service_runners / group runners) | ||||
|     PR->>RU: Start job (payload) | ||||
|     RU-->>PR: Job started (started_at) | ||||
|     PR->>DB: Update REQUESTS (status=running, started_at) | ||||
|     RU-->>PR: Job finished (duration, result) | ||||
|     PR->>DB: Update REQUESTS (status=succeeded, ended_at) | ||||
|     API->>DB: Insert BILLING_LEDGER (debit per effective price) | ||||
|     DB-->>API: Ledger entry id | ||||
|     API-->>AC: Return result + charge info | ||||
| ``` | ||||
|  | ||||
| ### 9.3 Pricing Resolution | ||||
|  | ||||
| ```mermaid | ||||
| flowchart TD | ||||
|     A[Input: provider_id, service_id, asset_code] --> B{Provider override exists for (service, asset_code)?} | ||||
|     B -- Yes --> P1[Use provider price/mode/max] | ||||
|     B -- No --> C{Service accepted currency override exists?} | ||||
|     C -- Yes --> P2[Use service currency price/mode] | ||||
|     C -- No --> P3[Use service defaults] | ||||
|     P1 --> OUT[Effective pricing] | ||||
|     P2 --> OUT | ||||
|     P3 --> OUT | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 10) Operational Notes | ||||
|  | ||||
| * **Secrets:** store a hash (e.g., `digest(secret,'sha256')`) rather than raw `secret`. Keep the original only client-side. | ||||
| * **Limits enforcement:** before insert of a debit ledger entry, compute period window (hour/day/month UTC or tenant TZ) and enforce `SUM(amount) + new_amount ≤ limit_amount`. | ||||
| * **Durations:** enforce `max_request_seconds` (effective) at orchestration and/or via DB trigger on `REQUESTS` when transitioning to `running/succeeded`. | ||||
| * **Routing:** prefer `provider_service_runners` when a request targets a service directly; otherwise use the union of runners from `provider_service_group_runners` for the group. | ||||
| * **Balances:** serve balance queries via the `account_balances` view or a materialized cache updated by triggers/jobs. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 11) Example Effective Pricing Query (sketch) | ||||
|  | ||||
| ```sql | ||||
| -- Inputs: :provider_id, :service_id, :asset_code | ||||
| WITH p AS ( | ||||
|   SELECT price_override, billing_mode_override, max_request_seconds_override | ||||
|   FROM provider_service_overrides | ||||
|   WHERE provider_id = :provider_id | ||||
|     AND service_id  = :service_id | ||||
|     AND (asset_code = :asset_code) | ||||
| ), | ||||
| sac AS ( | ||||
|   SELECT price_override, billing_mode_override | ||||
|   FROM service_accepted_currencies | ||||
|   WHERE service_id = :service_id AND asset_code = :asset_code | ||||
| ), | ||||
| svc AS ( | ||||
|   SELECT default_price AS price, default_billing_mode AS mode, max_request_seconds | ||||
|   FROM services WHERE id = :service_id | ||||
| ) | ||||
| SELECT | ||||
|   COALESCE(p.price_override, sac.price_override, svc.price)                AS effective_price, | ||||
|   COALESCE(p.billing_mode_override, sac.billing_mode_override, svc.mode)   AS effective_mode, | ||||
|   COALESCE(p.max_request_seconds_override, svc.max_request_seconds)        AS effective_max_seconds; | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 12) Indices (non-exhaustive) | ||||
|  | ||||
| * `services(default_currency)` | ||||
| * `service_accepted_currencies(service_id)` | ||||
| * `provider_service_overrides(service_id, provider_id)` | ||||
| * `requests(account_id)`, `requests(provider_id)`, `requests(service_id)` | ||||
| * `billing_ledger(account_id, asset_code)` | ||||
| * `subscriptions(account_id) WHERE active` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 13) Migration & Compatibility | ||||
|  | ||||
| * Prefer additive migrations (new columns/tables) to avoid downtime. | ||||
| * Use `ENUM` via `CREATE TYPE`; when extending, plan for `ALTER TYPE ... ADD VALUE`. | ||||
| * For high-write ledgers, consider partitioning `billing_ledger` by `created_at` (monthly) and indexing partitions. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 14) Non-Goals | ||||
|  | ||||
| * Wallet custody and on-chain settlement are out of scope. | ||||
| * SLA tracking and detailed observability (metrics/log schema) are not part of this spec. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 15) Acceptance Criteria | ||||
|  | ||||
| * Can represent services, groups, and providers with currency-specific pricing. | ||||
| * Can route requests to runners by service or group. | ||||
| * Can authorize usage via subscriptions, enforce spend limits, and record charges. | ||||
| * Can reconstruct balances and audit via append-only ledger. | ||||
|  | ||||
| --- | ||||
|  | ||||
| **End of Spec** | ||||
							
								
								
									
										225
									
								
								specs/billingmanager_research/conceptnote.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								specs/billingmanager_research/conceptnote.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | ||||
|  | ||||
| # Concept Note: Generic Billing & Tracking Framework | ||||
|  | ||||
| ## 1) Purpose | ||||
|  | ||||
| The model is designed to support a **flexible, generic, and auditable** billing environment that can be applied across diverse services and providers — from compute time billing to per-request API usage, across multiple currencies, with dynamic provider-specific overrides. | ||||
|  | ||||
| It is **not tied to a single business domain** — the same framework can be used for: | ||||
|  | ||||
| * Cloud compute time (per second) | ||||
| * API transactions (per request) | ||||
| * Data transfer charges | ||||
| * Managed service subscriptions | ||||
| * Brokered third-party service reselling | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 2) Key Concepts | ||||
|  | ||||
| ### 2.1 Accounts | ||||
|  | ||||
| An **account** represents an economic actor in the system — typically a customer or a service provider. | ||||
|  | ||||
| * Identified by a **public key** (for authentication & cryptographic signing). | ||||
| * Every billing action traces back to an account. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.2 Currencies & Asset Codes | ||||
|  | ||||
| The system supports **multiple currencies** (crypto or fiat) via **asset codes**. | ||||
|  | ||||
| * Asset codes identify the unit of billing (e.g. `USDC-ETH`, `EUR`, `LND`). | ||||
| * Currencies are **decoupled from services** so you can add or remove supported assets at any time. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.3 Services & Groups | ||||
|  | ||||
| * **Service** = a billable offering (e.g., "Speech-to-Text", "VM Hosting"). | ||||
|  | ||||
|   * Has a **billing mode** (`per_second` or `per_request`). | ||||
|   * Has a **default price** and **default currency**. | ||||
|   * Supports **multiple accepted currencies** with optional per-currency pricing overrides. | ||||
|   * Has execution constraints (e.g. `max_request_seconds`). | ||||
|   * Includes structured schemas for request payloads. | ||||
|  | ||||
| * **Service Group** = a logical grouping of services. | ||||
|  | ||||
|   * Groups make it easy to **bundle related services** and manage them together. | ||||
|   * Providers can offer entire groups rather than individual services. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.4 Service Providers | ||||
|  | ||||
| A **service provider** is an **account** that offers services or service groups. | ||||
| They can: | ||||
|  | ||||
| * Override **pricing** for their offered services (per currency). | ||||
| * Route requests to their own **runners** (execution agents). | ||||
| * Manage multiple **service groups** under one provider identity. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.5 Runners | ||||
|  | ||||
| A **runner** is an execution agent — a node, VM, or service endpoint that can fulfill requests. | ||||
|  | ||||
| * Identified by an **IPv6 address** (supports Mycelium or other overlay networks). | ||||
| * Can be owned by one or multiple providers. | ||||
| * Providers map **services/groups → runners** to define routing. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.6 Subscriptions | ||||
|  | ||||
| A **subscription** is **the authorization mechanism** for usage and spending control: | ||||
|  | ||||
| * Links an **account** to a **service** or **service group**. | ||||
| * Defines **spending limits** (amount, currency, period: hour/day/month). | ||||
| * Restricts which **providers** are allowed to serve the subscription. | ||||
| * Uses a **secret** chosen by the subscriber — providers use this to claim charges. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.7 Requests | ||||
|  | ||||
| A **request** represents a single execution under a subscription: | ||||
|  | ||||
| * Tied to **account**, **subscription**, **provider**, **service**, and optionally **runner**. | ||||
| * Has **status** (`pending`, `running`, `succeeded`, `failed`, `canceled`). | ||||
| * Records start/end times for duration-based billing. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 2.8 Billing Ledger | ||||
|  | ||||
| The **ledger** is **append-only** — the source of truth for all charges and credits. | ||||
|  | ||||
| * Each entry records: | ||||
|  | ||||
|   * `amount` (positive = debit, negative = credit/refund) | ||||
|   * `asset_code` | ||||
|   * Links to `account`, `provider`, `service`, and/or `request` | ||||
| * From the ledger, **balances** can be reconstructed at any time. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 3) How Billing Works — Step by Step | ||||
|  | ||||
| ### 3.1 Setup | ||||
|  | ||||
| 1. **Define services** with default pricing & schemas. | ||||
| 2. **Define currencies** and accepted currencies for services. | ||||
| 3. **Group services** into service groups. | ||||
| 4. **Onboard providers** (accounts) and associate them with service groups. | ||||
| 5. **Assign runners** to services or groups for execution routing. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 3.2 Subscription Creation | ||||
|  | ||||
| 1. Customer **creates a subscription**: | ||||
|  | ||||
|    * Chooses service or service group. | ||||
|    * Sets **spending limit** (amount, currency, period). | ||||
|    * Chooses **secret**. | ||||
|    * Selects **allowed providers**. | ||||
| 2. Subscription is stored in DB. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 3.3 Request Execution | ||||
|  | ||||
| 1. Customer sends a request to broker/API with: | ||||
|  | ||||
|    * `subscription_id` | ||||
|    * Target `service_id` | ||||
|    * Payload + signature using account pubkey. | ||||
| 2. Broker: | ||||
|  | ||||
|    * Validates **subscription active**. | ||||
|    * Validates **provider allowed**. | ||||
|    * Checks **spend limit** hasn’t been exceeded for current period. | ||||
|    * Resolves **effective price** via: | ||||
|  | ||||
|      1. Provider override (currency-specific) | ||||
|      2. Service accepted currency override | ||||
|      3. Service default | ||||
| 3. Broker selects **runner** from provider’s routing tables. | ||||
| 4. Runner executes request and returns result. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 3.4 Billing Entry | ||||
|  | ||||
| 1. When the request completes: | ||||
|  | ||||
|    * If `per_second` mode → calculate `duration × rate`. | ||||
|    * If `per_request` mode → apply flat rate. | ||||
| 2. Broker **inserts ledger entry**: | ||||
|  | ||||
|    * Debit from customer account. | ||||
|    * Credit to provider account (can be separate entries or aggregated). | ||||
| 3. Ledger is append-only — historical billing cannot be altered. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### 3.5 Balance & Tracking | ||||
|  | ||||
| * **Current balances** are a sum of all ledger entries per account+currency. | ||||
| * Spend limits are enforced by **querying the ledger** for the current period before each charge. | ||||
| * Audit trails are guaranteed via immutable ledger entries. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 4) Why This is Generic & Reusable | ||||
|  | ||||
| This design **decouples**: | ||||
|  | ||||
| * **Service definition** from **provider pricing** → multiple providers can sell the same service at different rates. | ||||
| * **Execution agents** (runners) from **service definitions** → easy scaling or outsourcing of execution. | ||||
| * **Billing rules** (per-second vs per-request) from **subscription limits** → same service can be sold in different billing modes. | ||||
| * **Currencies** from the service → enabling multi-asset billing without changing the service definition. | ||||
|  | ||||
| Because of these separations, you can: | ||||
|  | ||||
| * Reuse the model for **compute**, **APIs**, **storage**, **SaaS features**, etc. | ||||
| * Plug in different **payment backends** (on-chain, centralized payment processor, prepaid balance). | ||||
| * Use the same model for **internal cost allocation** or **external customer billing**. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 5) Potential Extensions | ||||
|  | ||||
| * **Prepaid model**: enforce that ledger debits can’t exceed balance. | ||||
| * **On-chain settlement**: periodically export ledger entries to blockchain transactions. | ||||
| * **Discount models**: percentage or fixed-amount discounts per subscription. | ||||
| * **Usage analytics**: aggregate requests/billing by time period, provider, or service. | ||||
| * **SLAs**: link billing adjustments to performance metrics in requests. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 6) Conceptual Diagram — Billing Flow | ||||
|  | ||||
| ```mermaid | ||||
| sequenceDiagram | ||||
|     participant C as Customer Account | ||||
|     participant B as Broker/API | ||||
|     participant P as Provider | ||||
|     participant R as Runner | ||||
|     participant DB as Ledger DB | ||||
|  | ||||
|     C->>B: Request(service, subscription, payload, secret) | ||||
|     B->>DB: Validate subscription & spend limit | ||||
|     DB-->>B: OK + effective pricing | ||||
|     B->>P: Forward request | ||||
|     P->>R: Execute request | ||||
|     R-->>P: Result + execution time | ||||
|     P->>B: Return result | ||||
|     B->>DB: Insert debit (customer) + credit (provider) | ||||
|     DB-->>B: Ledger updated | ||||
|     B-->>C: Return result + charge info | ||||
| ``` | ||||
							
								
								
									
										234
									
								
								specs/billingmanager_research/schema.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								specs/billingmanager_research/schema.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,234 @@ | ||||
| -- Enable useful extensions (optional) | ||||
| CREATE EXTENSION IF NOT EXISTS pgcrypto;   -- for digests/hashes if you want | ||||
| CREATE EXTENSION IF NOT EXISTS btree_gist; -- for exclusion/partial indexes | ||||
|  | ||||
| -- ========================= | ||||
| -- Core: Accounts & Currency | ||||
| -- ========================= | ||||
|  | ||||
| CREATE TABLE accounts ( | ||||
|   id               BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   pubkey           BYTEA NOT NULL UNIQUE, | ||||
|   display_name     TEXT, | ||||
|   created_at       TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   CHECK (id >= 0) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE currencies ( | ||||
|   asset_code       TEXT PRIMARY KEY,               -- e.g. "USDC-ETH", "EUR", "LND" | ||||
|   name             TEXT NOT NULL, | ||||
|   symbol           TEXT,                           -- e.g. "$", "€" | ||||
|   decimals         INT  NOT NULL DEFAULT 2,        -- how many decimal places | ||||
|   UNIQUE (name) | ||||
| ); | ||||
|  | ||||
| -- ========================= | ||||
| -- Services & Groups | ||||
| -- ========================= | ||||
|  | ||||
| CREATE TYPE billing_mode AS ENUM ('per_second', 'per_request'); | ||||
|  | ||||
| CREATE TABLE services ( | ||||
|   id                   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   name                 TEXT NOT NULL UNIQUE, | ||||
|   description          TEXT, | ||||
|   default_billing_mode billing_mode NOT NULL, | ||||
|   default_price        NUMERIC(38, 18) NOT NULL,   -- default price in "unit currency" (see accepted currencies) | ||||
|   default_currency     TEXT NOT NULL REFERENCES currencies(asset_code) ON UPDATE CASCADE, | ||||
|   max_request_seconds  INTEGER,                    -- nullable means no cap | ||||
|   schema_heroscript    TEXT, | ||||
|   schema_json          JSONB, | ||||
|   created_at           TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   CHECK (id >= 0), | ||||
|   CHECK (default_price >= 0), | ||||
|   CHECK (max_request_seconds IS NULL OR max_request_seconds > 0) | ||||
| ); | ||||
|  | ||||
| -- Accepted currencies for a service (subset + optional specific price per currency) | ||||
| CREATE TABLE service_accepted_currencies ( | ||||
|   service_id      BIGINT NOT NULL REFERENCES services(id) ON DELETE CASCADE, | ||||
|   asset_code      TEXT   NOT NULL REFERENCES currencies(asset_code) ON UPDATE CASCADE, | ||||
|   price_override  NUMERIC(38, 18),                 -- if set, overrides default_price for this currency | ||||
|   billing_mode_override billing_mode,              -- if set, overrides default_billing_mode | ||||
|   PRIMARY KEY (service_id, asset_code), | ||||
|   CHECK (price_override IS NULL OR price_override >= 0) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE service_groups ( | ||||
|   id               BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   name             TEXT NOT NULL UNIQUE, | ||||
|   description      TEXT, | ||||
|   created_at       TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   CHECK (id >= 0) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE service_group_members ( | ||||
|   group_id   BIGINT NOT NULL REFERENCES service_groups(id) ON DELETE CASCADE, | ||||
|   service_id BIGINT NOT NULL REFERENCES services(id)       ON DELETE RESTRICT, | ||||
|   PRIMARY KEY (group_id, service_id) | ||||
| ); | ||||
|  | ||||
| -- ========================= | ||||
| -- Providers, Runners, Routing | ||||
| -- ========================= | ||||
|  | ||||
| CREATE TABLE service_providers ( | ||||
|   id               BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   account_id       BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, -- provider is an account | ||||
|   name             TEXT NOT NULL, | ||||
|   description      TEXT, | ||||
|   created_at       TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   UNIQUE (name), | ||||
|   CHECK (id >= 0) | ||||
| ); | ||||
|  | ||||
| -- Providers can offer groups (which imply their services) | ||||
| CREATE TABLE provider_service_groups ( | ||||
|   provider_id BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, | ||||
|   group_id    BIGINT NOT NULL REFERENCES service_groups(id)    ON DELETE CASCADE, | ||||
|   PRIMARY KEY (provider_id, group_id) | ||||
| ); | ||||
|  | ||||
| -- Providers may set per-service overrides (price/mode/max seconds) (optionally per currency) | ||||
| CREATE TABLE provider_service_overrides ( | ||||
|   provider_id     BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, | ||||
|   service_id      BIGINT NOT NULL REFERENCES services(id)          ON DELETE CASCADE, | ||||
|   asset_code      TEXT   REFERENCES currencies(asset_code) ON UPDATE CASCADE, | ||||
|   price_override  NUMERIC(38, 18), | ||||
|   billing_mode_override billing_mode, | ||||
|   max_request_seconds_override INTEGER, | ||||
|   PRIMARY KEY (provider_id, service_id, asset_code), | ||||
|   CHECK (price_override IS NULL OR price_override >= 0), | ||||
|   CHECK (max_request_seconds_override IS NULL OR max_request_seconds_override > 0) | ||||
| ); | ||||
|  | ||||
| -- Runners | ||||
| CREATE TABLE runners ( | ||||
|   id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   address      INET NOT NULL,            -- IPv6 (INET supports both IPv4/IPv6; require v6 via CHECK below if you like) | ||||
|   name         TEXT NOT NULL, | ||||
|   description  TEXT, | ||||
|   pubkey       BYTEA,                    -- optional | ||||
|   created_at   TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   UNIQUE (address), | ||||
|   CHECK (id >= 0), | ||||
|   CHECK (family(address) = 6)            -- ensure IPv6 | ||||
| ); | ||||
|  | ||||
| -- Runner ownership: a runner can be owned by multiple providers | ||||
| CREATE TABLE runner_owners ( | ||||
|   runner_id    BIGINT NOT NULL REFERENCES runners(id)           ON DELETE CASCADE, | ||||
|   provider_id  BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, | ||||
|   PRIMARY KEY (runner_id, provider_id) | ||||
| ); | ||||
|  | ||||
| -- Routing: link providers' services to specific runners | ||||
| CREATE TABLE provider_service_runners ( | ||||
|   provider_id  BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, | ||||
|   service_id   BIGINT NOT NULL REFERENCES services(id)          ON DELETE CASCADE, | ||||
|   runner_id    BIGINT NOT NULL REFERENCES runners(id)           ON DELETE CASCADE, | ||||
|   PRIMARY KEY (provider_id, service_id, runner_id) | ||||
| ); | ||||
|  | ||||
| -- Routing: link providers' service groups to runners | ||||
| CREATE TABLE provider_service_group_runners ( | ||||
|   provider_id  BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, | ||||
|   group_id     BIGINT NOT NULL REFERENCES service_groups(id)    ON DELETE CASCADE, | ||||
|   runner_id    BIGINT NOT NULL REFERENCES runners(id)           ON DELETE CASCADE, | ||||
|   PRIMARY KEY (provider_id, group_id, runner_id) | ||||
| ); | ||||
|  | ||||
| -- ========================= | ||||
| -- Subscriptions & Spend Control | ||||
| -- ========================= | ||||
|  | ||||
| CREATE TYPE spend_period AS ENUM ('hour', 'day', 'month'); | ||||
|  | ||||
| -- A subscription ties an account to a specific service OR a service group, with spend limits and allowed providers | ||||
| CREATE TABLE subscriptions ( | ||||
|   id                   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   account_id           BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, | ||||
|   service_id           BIGINT REFERENCES services(id) ON DELETE CASCADE, | ||||
|   group_id             BIGINT REFERENCES service_groups(id) ON DELETE CASCADE, | ||||
|   secret               BYTEA NOT NULL,             -- caller-chosen secret (consider storing a hash instead) | ||||
|   subscription_data    JSONB,                      -- arbitrary client-supplied info | ||||
|   limit_amount         NUMERIC(38, 18),            -- allowed spend in the selected currency per period | ||||
|   limit_currency       TEXT REFERENCES currencies(asset_code) ON UPDATE CASCADE, | ||||
|   limit_period         spend_period,               -- period for the limit | ||||
|   active               BOOLEAN NOT NULL DEFAULT TRUE, | ||||
|   created_at           TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   -- Ensure exactly one of service_id or group_id | ||||
|   CHECK ( (service_id IS NOT NULL) <> (group_id IS NOT NULL) ), | ||||
|   CHECK (limit_amount IS NULL OR limit_amount >= 0), | ||||
|   CHECK (id >= 0) | ||||
| ); | ||||
|  | ||||
| -- Providers that are allowed to serve under a subscription | ||||
| CREATE TABLE subscription_providers ( | ||||
|   subscription_id BIGINT NOT NULL REFERENCES subscriptions(id)    ON DELETE CASCADE, | ||||
|   provider_id     BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE CASCADE, | ||||
|   PRIMARY KEY (subscription_id, provider_id) | ||||
| ); | ||||
|  | ||||
| -- ========================= | ||||
| -- Usage, Requests & Billing | ||||
| -- ========================= | ||||
|  | ||||
| -- A request lifecycle record (optional but useful for auditing and max duration enforcement) | ||||
| CREATE TYPE request_status AS ENUM ('pending', 'running', 'succeeded', 'failed', 'canceled'); | ||||
|  | ||||
| CREATE TABLE requests ( | ||||
|   id               BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   account_id       BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, | ||||
|   subscription_id  BIGINT NOT NULL REFERENCES subscriptions(id) ON DELETE RESTRICT, | ||||
|   provider_id      BIGINT NOT NULL REFERENCES service_providers(id) ON DELETE RESTRICT, | ||||
|   service_id       BIGINT NOT NULL REFERENCES services(id) ON DELETE RESTRICT, | ||||
|   runner_id        BIGINT REFERENCES runners(id) ON DELETE SET NULL, | ||||
|   request_schema   JSONB,                         -- concrete task payload (conforms to schema_json/heroscript) | ||||
|   started_at       TIMESTAMPTZ, | ||||
|   ended_at         TIMESTAMPTZ, | ||||
|   status           request_status NOT NULL DEFAULT 'pending', | ||||
|   created_at       TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   CHECK (id >= 0), | ||||
|   CHECK (ended_at IS NULL OR started_at IS NULL OR ended_at >= started_at) | ||||
| ); | ||||
|  | ||||
| -- Billing ledger (debits/credits). Positive amount = debit to account (charge). Negative = credit/refund. | ||||
| CREATE TYPE ledger_entry_type AS ENUM ('debit', 'credit', 'adjustment'); | ||||
|  | ||||
| CREATE TABLE billing_ledger ( | ||||
|   id             BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||||
|   account_id     BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, | ||||
|   provider_id    BIGINT REFERENCES service_providers(id) ON DELETE SET NULL, | ||||
|   service_id     BIGINT REFERENCES services(id)          ON DELETE SET NULL, | ||||
|   request_id     BIGINT REFERENCES requests(id)          ON DELETE SET NULL, | ||||
|   amount         NUMERIC(38, 18) NOT NULL,               -- positive for debit, negative for credit | ||||
|   asset_code     TEXT NOT NULL REFERENCES currencies(asset_code) ON UPDATE CASCADE, | ||||
|   entry_type     ledger_entry_type NOT NULL, | ||||
|   description    TEXT, | ||||
|   created_at     TIMESTAMPTZ NOT NULL DEFAULT now(), | ||||
|   CHECK (id >= 0) | ||||
| ); | ||||
|  | ||||
| -- Optional: running balances per account/currency (materialized view or real-time view) | ||||
| -- This is a plain view; for performance, you might maintain a cached table. | ||||
| CREATE VIEW account_balances AS | ||||
| SELECT | ||||
|   account_id, | ||||
|   asset_code, | ||||
|   SUM(amount) AS balance | ||||
| FROM billing_ledger | ||||
| GROUP BY account_id, asset_code; | ||||
|  | ||||
| -- ========================= | ||||
| -- Helpful Indexes | ||||
| -- ========================= | ||||
|  | ||||
| CREATE INDEX idx_services_default_currency ON services(default_currency); | ||||
| CREATE INDEX idx_service_accepted_currencies_service ON service_accepted_currencies(service_id); | ||||
| CREATE INDEX idx_provider_overrides_service ON provider_service_overrides(service_id); | ||||
| CREATE INDEX idx_requests_account ON requests(account_id); | ||||
| CREATE INDEX idx_requests_provider ON requests(provider_id); | ||||
| CREATE INDEX idx_requests_service ON requests(service_id); | ||||
| CREATE INDEX idx_billing_account_currency ON billing_ledger(account_id, asset_code); | ||||
| CREATE INDEX idx_subscriptions_account_active ON subscriptions(account_id) WHERE active; | ||||
							
								
								
									
										266
									
								
								specs/billingmanager_research/summary.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								specs/billingmanager_research/summary.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,266 @@ | ||||
| # Billing Logic — Whiteboard Version (for Devs) | ||||
|  | ||||
| ## 1) Inputs You Always Need | ||||
|  | ||||
| * `account_id`, `subscription_id` | ||||
| * `service_id` (or group → resolved to a service at dispatch) | ||||
| * `provider_id`, `asset_code` | ||||
| * `payload` (validated against service schema) | ||||
| * (Optional) `runner_id` | ||||
| * Idempotency key for the request (client-provided) | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 2) Gatekeeping (Hard Checks) | ||||
|  | ||||
| 1. **Subscription** | ||||
|  | ||||
| * Must be `active`. | ||||
| * Must target **exactly one** of {service, group}. | ||||
| * If group: ensure `service_id` is a member. | ||||
|  | ||||
| 2. **Provider Allowlist** | ||||
|  | ||||
| * If `subscription_providers` exists → `provider_id` must be listed. | ||||
|  | ||||
| 3. **Spend Limit** (if set) | ||||
|  | ||||
| * Compute window by `limit_period` (`hour`/`day`/`month`, UTC unless tenant TZ). | ||||
| * Current period spend = `SUM(ledger.amount WHERE account & currency & period)`. | ||||
| * `current_spend + estimated_charge ≤ limit_amount`. | ||||
|  | ||||
| 4. **Max Duration** (effective; see §3): | ||||
|  | ||||
| * If billing mode is `per_second`, reject if requested/max exceeds effective cap. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 3) Effective Pricing (Single Resolution Function) | ||||
|  | ||||
| Inputs: `provider_id`, `service_id`, `asset_code` | ||||
|  | ||||
| Precedence: | ||||
|  | ||||
| 1. `provider_service_overrides` for `(service_id, asset_code)` | ||||
| 2. `service_accepted_currencies` for `(service_id, asset_code)` | ||||
| 3. `services` defaults | ||||
|  | ||||
| Outputs: | ||||
|  | ||||
| * `effective_billing_mode ∈ {per_request, per_second}` | ||||
| * `effective_price` (NUMERIC) | ||||
| * `effective_max_request_seconds` (nullable) | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 4) Request Lifecycle (States) | ||||
|  | ||||
| * `pending` → `running` → (`succeeded` | `failed` | `canceled`) | ||||
| * Timestamps: set `started_at` on `running`, `ended_at` on terminal states. | ||||
| * Enforce `ended_at ≥ started_at` and `duration ≤ effective_max_request_seconds` (if set). | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 5) Charging Rules | ||||
|  | ||||
| ### A) Per Request | ||||
|  | ||||
| ``` | ||||
| charge = effective_price | ||||
| ``` | ||||
|  | ||||
| ### B) Per Second | ||||
|  | ||||
| ``` | ||||
| duration_seconds = ceil(extract(epoch from (ended_at - started_at))) | ||||
| charge = duration_seconds * effective_price | ||||
| ``` | ||||
|  | ||||
| * Cap with `effective_max_request_seconds` if present. | ||||
| * If ended early/failed before `started_at`: charge = 0. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 6) Idempotency & Atomicity | ||||
|  | ||||
| * **Idempotency key** per `(account_id, subscription_id, provider_id, service_id, request_external_id)`; store on `requests` and enforce unique index. | ||||
| * **Single transaction** to: | ||||
|  | ||||
|   1. finalize `REQUESTS` status + timestamps, | ||||
|   2. insert **one** debit entry into `billing_ledger`. | ||||
| * Never mutate ledger entries; use compensating **credit** entries for adjustments/refunds. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 7) Spend-Limit Enforcement (Before Charging) | ||||
|  | ||||
| Pseudocode (SQL-ish): | ||||
|  | ||||
| ```sql | ||||
| WITH window AS ( | ||||
|   SELECT tsrange(period_start(:limit_period), period_end(:limit_period)) AS w | ||||
| ), | ||||
| spent AS ( | ||||
|   SELECT COALESCE(SUM(amount), 0) AS total | ||||
|   FROM billing_ledger, window | ||||
|   WHERE account_id = :account_id | ||||
|     AND asset_code = :asset_code | ||||
|     AND created_at <@ (SELECT w FROM window) | ||||
| ), | ||||
| check AS ( | ||||
|   SELECT (spent.total + :estimated_charge) <= :limit_amount AS ok FROM spent | ||||
| ) | ||||
| SELECT ok FROM check; | ||||
| ``` | ||||
|  | ||||
| * If not ok → reject before dispatch, or allow but **set hard cap** on max seconds and auto-stop at limit. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 8) Suggested DB Operations (Happy Path) | ||||
|  | ||||
| 1. **Create request** | ||||
|  | ||||
| ```sql | ||||
| INSERT INTO requests (...) | ||||
| VALUES (...) | ||||
| ON CONFLICT (idempotency_key) DO NOTHING | ||||
| RETURNING id; | ||||
| ``` | ||||
|  | ||||
| 2. **Start execution** | ||||
|  | ||||
| ```sql | ||||
| UPDATE requests | ||||
| SET status='running', started_at=now() | ||||
| WHERE id=:id AND status='pending'; | ||||
| ``` | ||||
|  | ||||
| 3. **Finish & bill** (single transaction) | ||||
|  | ||||
| ```sql | ||||
| BEGIN; | ||||
|  | ||||
| -- lock for update to avoid double-billing | ||||
| UPDATE requests | ||||
| SET status=:final_status, ended_at=now() | ||||
| WHERE id=:id AND status='running' | ||||
| RETURNING started_at, ended_at; | ||||
|  | ||||
| -- compute charge in app (see §5), re-check spend window here | ||||
|  | ||||
| INSERT INTO billing_ledger ( | ||||
|   account_id, provider_id, service_id, request_id, | ||||
|   amount, asset_code, entry_type, description | ||||
| ) VALUES ( | ||||
|   :account_id, :provider_id, :service_id, :id, | ||||
|   :charge, :asset_code, 'debit', :desc | ||||
| ); | ||||
|  | ||||
| COMMIT; | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 9) Balances & Reporting | ||||
|  | ||||
| * **Current balance** = `SUM(billing_ledger.amount) GROUP BY account_id, asset_code`. | ||||
| * Keep a **view** or **materialized view**; refresh asynchronously if needed. | ||||
| * Never rely on cached balance for hard checks — re-check within the billing transaction if **prepaid** semantics are required. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 10) Error & Edge Rules | ||||
|  | ||||
| * If runner fails before `running` → no charge. | ||||
| * If runner starts, then fails: | ||||
|  | ||||
|   * **per\_second**: bill actual seconds (can be 0). | ||||
|   * **per\_request**: default is **no charge** unless policy says otherwise; if charging partials, document it. | ||||
| * Partial refunds/adjustments → insert **negative** ledger entries (type `credit`/`adjustment`) tied to the original `request_id`. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 11) Minimal Pricing Resolver (Sketch) | ||||
|  | ||||
| ```sql | ||||
| WITH p AS ( | ||||
|   SELECT price_override AS price, | ||||
|          billing_mode_override AS mode, | ||||
|          max_request_seconds_override AS maxsec | ||||
|   FROM provider_service_overrides | ||||
|   WHERE provider_id = :pid AND service_id = :sid AND asset_code = :asset | ||||
|   LIMIT 1 | ||||
| ), | ||||
| sac AS ( | ||||
|   SELECT price_override AS price, | ||||
|          billing_mode_override AS mode | ||||
|   FROM service_accepted_currencies | ||||
|   WHERE service_id = :sid AND asset_code = :asset | ||||
|   LIMIT 1 | ||||
| ), | ||||
| svc AS ( | ||||
|   SELECT default_price AS price, | ||||
|          default_billing_mode AS mode, | ||||
|          max_request_seconds AS maxsec | ||||
|   FROM services WHERE id = :sid | ||||
| ) | ||||
| SELECT | ||||
|   COALESCE(p.price, sac.price, svc.price) AS price, | ||||
|   COALESCE(p.mode,  sac.mode,  svc.mode)  AS mode, | ||||
|   COALESCE(p.maxsec, svc.maxsec)          AS max_seconds; | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 12) Mermaid — Decision Trees | ||||
|  | ||||
| ### Pricing & Duration | ||||
|  | ||||
| ```mermaid | ||||
| flowchart TD | ||||
|     A[provider_id, service_id, asset_code] --> B{Provider override exists?} | ||||
|     B -- yes --> P[Use provider price/mode/max] | ||||
|     B -- no --> C{Service currency override?} | ||||
|     C -- yes --> S[Use service currency price/mode] | ||||
|     C -- no --> D[Use service defaults] | ||||
|     P --> OUT[effective price/mode/max] | ||||
|     S --> OUT | ||||
|     D --> OUT | ||||
| ``` | ||||
|  | ||||
| ### Spend Check & Charge | ||||
|  | ||||
| ```mermaid | ||||
| flowchart TD | ||||
|     S[Has subscription limit?] -->|No| D1[Dispatch] | ||||
|     S -->|Yes| C{current_spend + est_charge <= limit?} | ||||
|     C -->|No| REJ[Reject or cap duration] | ||||
|     C -->|Yes| D1[Dispatch] | ||||
|     D1 --> RUN[Run request] | ||||
|     RUN --> DONE[Finalize + insert ledger] | ||||
| ``` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 13) Security Posture | ||||
|  | ||||
| * Store **hash of subscription secret**; compare hash on use. | ||||
| * Sign client requests with **account pubkey**; verify before dispatch. | ||||
| * Limit **request schema** to validated fields; reject unknowns. | ||||
| * Enforce **IPv6** for runners where required. | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 14) What To Implement First | ||||
|  | ||||
| 1. Pricing resolver (single function). | ||||
| 2. Spend-window checker (single query). | ||||
| 3. Request lifecycle + idempotency. | ||||
| 4. Ledger write (append-only) + balances view. | ||||
|  | ||||
| Everything else layers on top. | ||||
|  | ||||
| --- | ||||
|  | ||||
| If you want, I can turn this into a small **README.md** with code blocks you can paste into the repo (plus a couple of SQL functions and example tests). | ||||
| @@ -24,16 +24,6 @@ pub enum CurrencyType { | ||||
| 	custom | ||||
| } | ||||
|  | ||||
| pub struct Price { | ||||
| pub mut: | ||||
| 	base_amount          f64 // Using f64 for Decimal | ||||
| 	base_currency        string | ||||
| 	display_currency     string | ||||
| 	display_amount       f64 // Using f64 for Decimal | ||||
| 	formatted_display    string | ||||
| 	conversion_rate      f64 // Using f64 for Decimal | ||||
| 	conversion_timestamp u64 // Unix timestamp | ||||
| } | ||||
|  | ||||
| pub struct MarketplaceCurrencyConfig { | ||||
| pub mut: | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user