//! Slice calculator service for automatic slice calculation from node capacity //! Follows the established builder pattern for consistent API design use crate::models::user::{NodeCapacity, FarmNode}; use rust_decimal::Decimal; use chrono::{DateTime, Utc}; use serde::{Serialize, Deserialize}; /// Base slice unit definition (1 vCPU, 4GB RAM, 200GB storage) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SliceUnit { pub cpu_cores: u32, // 1 pub memory_gb: u32, // 4 pub storage_gb: u32, // 200 } impl Default for SliceUnit { fn default() -> Self { Self { cpu_cores: 1, memory_gb: 4, storage_gb: 200, } } } /// Calculated slice combination from node capacity #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SliceCombination { pub id: String, pub multiplier: u32, // How many base slices this uses pub cpu_cores: u32, // Slice-specific resource pub memory_gb: u32, // Slice-specific resource pub storage_gb: u32, // Slice-specific resource pub quantity_available: u32, // How many of this combination available pub price_per_hour: Decimal, pub base_slices_required: u32, // Inherited from parent node pub node_uptime_percentage: f64, pub node_bandwidth_mbps: u32, pub node_location: String, pub node_certification_type: String, pub node_id: String, pub resource_provider_email: String, } /// Track individual slice rentals #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SliceAllocation { pub allocation_id: String, pub slice_combination_id: String, pub renter_email: String, pub base_slices_used: u32, pub rental_start: DateTime, pub rental_end: Option>, pub status: AllocationStatus, pub monthly_cost: Decimal, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AllocationStatus { Active, Expired, Cancelled, } /// Pricing configuration for node slices #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SlicePricing { pub base_price_per_hour: Decimal, // Price for 1 base slice per hour pub currency: String, pub pricing_multiplier: Decimal, // ResourceProvider can adjust pricing (0.5x - 2.0x) } impl Default for SlicePricing { fn default() -> Self { Self { base_price_per_hour: Decimal::from(1), // $1 per hour for base slice currency: "USD".to_string(), pricing_multiplier: Decimal::from(1), } } } /// Service for slice calculations following builder pattern #[derive(Clone)] pub struct SliceCalculatorService { base_slice: SliceUnit, pricing_limits: PricingLimits, } /// Platform-enforced pricing limits #[derive(Debug, Clone)] pub struct PricingLimits { pub min_price_per_hour: Decimal, // e.g., $0.10 pub max_price_per_hour: Decimal, // e.g., $10.00 } impl Default for PricingLimits { fn default() -> Self { Self { min_price_per_hour: Decimal::from_str_exact("0.10").unwrap(), max_price_per_hour: Decimal::from_str_exact("10.00").unwrap(), } } } /// Builder for SliceCalculatorService #[derive(Default)] pub struct SliceCalculatorServiceBuilder { base_slice: Option, pricing_limits: Option, } impl SliceCalculatorServiceBuilder { pub fn new() -> Self { Self::default() } pub fn base_slice(mut self, base_slice: SliceUnit) -> Self { self.base_slice = Some(base_slice); self } pub fn pricing_limits(mut self, limits: PricingLimits) -> Self { self.pricing_limits = Some(limits); self } pub fn build(self) -> Result { Ok(SliceCalculatorService { base_slice: self.base_slice.unwrap_or_default(), pricing_limits: self.pricing_limits.unwrap_or_default(), }) } } impl SliceCalculatorService { pub fn builder() -> SliceCalculatorServiceBuilder { SliceCalculatorServiceBuilder::new() } /// Calculate maximum base slices from node capacity pub fn calculate_max_base_slices(&self, capacity: &NodeCapacity) -> u32 { let cpu_slices = capacity.cpu_cores as u32 / self.base_slice.cpu_cores; let memory_slices = capacity.memory_gb as u32 / self.base_slice.memory_gb; let storage_slices = capacity.storage_gb as u32 / self.base_slice.storage_gb; // Return the limiting factor std::cmp::min(std::cmp::min(cpu_slices, memory_slices), storage_slices) } /// Generate all possible slice combinations from available base slices pub fn generate_slice_combinations( &self, max_base_slices: u32, allocated_slices: u32, node: &FarmNode, resource_provider_email: &str ) -> Vec { let available_base_slices = max_base_slices.saturating_sub(allocated_slices); let mut combinations = Vec::new(); if available_base_slices == 0 { return combinations; } // Generate practical slice combinations up to a reasonable limit // We'll generate combinations for multipliers 1x, 2x, 3x, 4x, 5x, 6x, 8x, 10x, 12x, 16x, 20x, 24x, 32x let practical_multipliers = vec![1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32]; for multiplier in practical_multipliers { // Skip if multiplier is larger than available slices if multiplier > available_base_slices { continue; } // Calculate how many complete units of this multiplier we can create let quantity = available_base_slices / multiplier; // Skip if we can't create at least one complete unit if quantity == 0 { continue; } let combination = SliceCombination { id: format!("{}x{}", quantity, multiplier), multiplier, cpu_cores: self.base_slice.cpu_cores * multiplier, memory_gb: self.base_slice.memory_gb * multiplier, storage_gb: self.base_slice.storage_gb * multiplier, quantity_available: quantity, price_per_hour: self.calculate_combination_price(multiplier, node.slice_pricing.as_ref() .and_then(|sp| serde_json::from_value(sp.clone()).ok()) .as_ref() .unwrap_or(&crate::services::slice_calculator::SlicePricing::default())), base_slices_required: multiplier, // Inherited from parent node node_uptime_percentage: node.uptime_percentage as f64, node_bandwidth_mbps: node.capacity.bandwidth_mbps as u32, node_location: node.location.clone(), node_certification_type: node.grid_data.as_ref() .map(|g| g.get("certification_type") .and_then(|cert| cert.as_str()) .unwrap_or("DIY") .to_string()) .unwrap_or_else(|| "DIY".to_string()), node_id: node.id.clone(), resource_provider_email: resource_provider_email.to_string(), }; combinations.push(combination); } // Sort by multiplier (smallest slices first) combinations.sort_by_key(|c| c.multiplier); combinations } /// Generate slice combinations with explicit SLA values (for user-defined SLAs) pub fn generate_slice_combinations_with_sla( &self, max_base_slices: u32, allocated_slices: u32, node: &FarmNode, resource_provider_email: &str, uptime_percentage: f64, bandwidth_mbps: u32, base_price_per_hour: Decimal ) -> Vec { let available_base_slices = max_base_slices.saturating_sub(allocated_slices); let mut combinations = Vec::new(); if available_base_slices == 0 { return combinations; } // Generate practical slice combinations up to a reasonable limit // We'll generate combinations for multipliers 1x, 2x, 3x, 4x, 5x, 6x, 8x, 10x, 12x, 16x, 20x, 24x, 32x let practical_multipliers = vec![1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32]; // Create custom pricing with user's base price let custom_pricing = SlicePricing { base_price_per_hour, currency: "USD".to_string(), pricing_multiplier: Decimal::from(1), }; for multiplier in practical_multipliers { // Skip if multiplier is larger than available slices if multiplier > available_base_slices { continue; } // Calculate how many complete units of this multiplier we can create let quantity = available_base_slices / multiplier; // Skip if we can't create at least one complete unit if quantity == 0 { continue; } let combination = SliceCombination { id: format!("{}x{}", quantity, multiplier), multiplier, cpu_cores: self.base_slice.cpu_cores * multiplier, memory_gb: self.base_slice.memory_gb * multiplier, storage_gb: self.base_slice.storage_gb * multiplier, quantity_available: quantity, price_per_hour: self.calculate_combination_price(multiplier, &custom_pricing), base_slices_required: multiplier, // Use explicit SLA values instead of inheriting from node node_uptime_percentage: uptime_percentage, node_bandwidth_mbps: bandwidth_mbps, node_location: node.location.clone(), node_certification_type: node.grid_data.as_ref() .map(|g| g.get("certification_type") .and_then(|cert| cert.as_str()) .unwrap_or("DIY") .to_string()) .unwrap_or_else(|| "DIY".to_string()), node_id: node.id.clone(), resource_provider_email: resource_provider_email.to_string(), }; combinations.push(combination); } // Sort by multiplier (smallest slices first) combinations.sort_by_key(|c| c.multiplier); combinations } /// Calculate price for a slice combination fn calculate_combination_price(&self, multiplier: u32, pricing: &SlicePricing) -> Decimal { pricing.base_price_per_hour * pricing.pricing_multiplier * Decimal::from(multiplier) } /// Update availability after rental pub fn update_availability_after_rental( &self, node: &mut FarmNode, rented_base_slices: u32, resource_provider_email: &str ) -> Result<(), String> { // Update allocated count node.allocated_base_slices += rented_base_slices as i32; // Recalculate available combinations let combinations = self.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, node, resource_provider_email ); node.available_combinations = combinations.iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); Ok(()) } /// Update availability after rental expiry pub fn update_availability_after_release( &self, node: &mut FarmNode, released_base_slices: u32, resource_provider_email: &str ) -> Result<(), String> { // Update allocated count node.allocated_base_slices = node.allocated_base_slices.saturating_sub(released_base_slices as i32); // Recalculate available combinations node.available_combinations = self.generate_slice_combinations( node.total_base_slices as u32, node.allocated_base_slices as u32, node, resource_provider_email ).iter() .map(|c| serde_json::to_value(c).unwrap_or_default()) .collect(); Ok(()) } /// Validate slice price within platform limits pub fn validate_slice_price(&self, price: Decimal) -> Result<(), String> { if price < self.pricing_limits.min_price_per_hour { return Err(format!("Price too low. Minimum: ${}/hour", self.pricing_limits.min_price_per_hour)); } if price > self.pricing_limits.max_price_per_hour { return Err(format!("Price too high. Maximum: ${}/hour", self.pricing_limits.max_price_per_hour)); } Ok(()) } } /// Slice rental record for users with deployment options #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SliceRental { pub rental_id: String, pub slice_combination_id: String, pub node_id: String, pub resource_provider_email: String, pub slice_allocation: SliceAllocation, pub total_cost: Decimal, pub payment_status: PaymentStatus, #[serde(default)] pub slice_format: String, #[serde(default)] pub user_email: String, #[serde(default)] pub status: String, #[serde(default)] pub start_date: Option>, #[serde(default)] pub rental_duration_days: Option, #[serde(default)] pub monthly_cost: Option, #[serde(default)] pub id: String, // NEW: Deployment information #[serde(default)] pub deployment_type: Option, // "vm" or "kubernetes" #[serde(default)] pub deployment_name: Option, #[serde(default)] pub deployment_config: Option, #[serde(default)] pub deployment_status: Option, // "Provisioning", "Active", "Stopped", "Failed" #[serde(default)] pub deployment_endpoint: Option, // Access URL/IP for the deployment #[serde(default)] pub deployment_metadata: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaymentStatus { Pending, Paid, Failed, Refunded, }