//! Slice rental service for managing slice rentals and availability //! Follows the established builder pattern for consistent API design use crate::services::slice_calculator::{SliceCalculatorService, SliceAllocation, SliceRental, AllocationStatus, PaymentStatus}; use crate::services::user_persistence::{UserPersistence, UserPersistentData}; use rust_decimal::Decimal; use rust_decimal::prelude::*; use std::str::FromStr; use chrono::Utc; use serde::{Serialize, Deserialize}; use std::fs::OpenOptions; /// Service for slice rental operations #[derive(Clone)] pub struct SliceRentalService { slice_calculator: SliceCalculatorService, enable_file_locking: bool, } /// Builder for SliceRentalService #[derive(Default)] pub struct SliceRentalServiceBuilder { slice_calculator: Option, enable_file_locking: Option, } impl SliceRentalServiceBuilder { pub fn new() -> Self { Self::default() } pub fn slice_calculator(mut self, slice_calculator: SliceCalculatorService) -> Self { self.slice_calculator = Some(slice_calculator); self } pub fn enable_file_locking(mut self, enabled: bool) -> Self { self.enable_file_locking = Some(enabled); self } pub fn build(self) -> Result { let slice_calculator = self.slice_calculator.unwrap_or_else(|| { SliceCalculatorService::builder().build().expect("Failed to create default SliceCalculatorService") }); Ok(SliceRentalService { slice_calculator, enable_file_locking: self.enable_file_locking.unwrap_or(true), }) } } impl SliceRentalService { pub fn builder() -> SliceRentalServiceBuilder { SliceRentalServiceBuilder::new() } /// Rent a slice combination from a farmer's node pub fn rent_slice_combination( &self, renter_email: &str, farmer_email: &str, node_id: &str, combination_id: &str, quantity: u32, rental_duration_hours: u32, ) -> Result { // Atomic operation with file locking to prevent conflicts if self.enable_file_locking { self.rent_with_file_lock(renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours) } else { self.rent_without_lock(renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours) } } /// Rent a slice combination with deployment options (VM/Kubernetes) pub fn rent_slice_combination_with_deployment( &self, renter_email: &str, farmer_email: &str, node_id: &str, combination_id: &str, quantity: u32, rental_duration_hours: u32, deployment_type: &str, deployment_name: &str, deployment_config: Option, ) -> Result { // First rent the slice combination let mut rental = self.rent_slice_combination( renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours )?; // Add deployment metadata to the rental rental.deployment_type = Some(deployment_type.to_string()); rental.deployment_name = Some(deployment_name.to_string()); rental.deployment_config = deployment_config; rental.deployment_status = Some("Provisioning".to_string()); // Save the enhanced rental to user's persistent data self.save_rental_to_user_data(renter_email, &rental)?; Ok(rental) } /// Get user's slice rentals pub fn get_user_slice_rentals(&self, user_email: &str) -> Vec { if let Some(persistent_data) = UserPersistence::load_user_data(user_email) { persistent_data.slice_rentals } else { Vec::new() } } /// Save rental to user's persistent data fn save_rental_to_user_data(&self, user_email: &str, rental: &SliceRental) -> Result<(), String> { let mut persistent_data = UserPersistence::load_user_data(user_email) .unwrap_or_else(|| UserPersistentData::default()); // Add or update the rental if let Some(existing_index) = persistent_data.slice_rentals.iter().position(|r| r.rental_id == rental.rental_id) { persistent_data.slice_rentals[existing_index] = rental.clone(); } else { persistent_data.slice_rentals.push(rental.clone()); } UserPersistence::save_user_data(&persistent_data) .map_err(|e| format!("Failed to save rental to user data: {}", e)) } /// Rent slice with file locking for atomic operations fn rent_with_file_lock( &self, renter_email: &str, farmer_email: &str, node_id: &str, combination_id: &str, quantity: u32, rental_duration_hours: u32, ) -> Result { // Create lock file let lock_file_path = format!("./user_data/.lock_{}_{}", farmer_email.replace("@", "_"), node_id); let _lock_file = OpenOptions::new() .create(true) .write(true) .open(&lock_file_path) .map_err(|e| format!("Failed to create lock file: {}", e))?; let result = self.rent_without_lock(renter_email, farmer_email, node_id, combination_id, quantity, rental_duration_hours); // Clean up lock file let _ = std::fs::remove_file(&lock_file_path); result } /// Rent slice without file locking fn rent_without_lock( &self, renter_email: &str, farmer_email: &str, node_id: &str, combination_id: &str, quantity: u32, rental_duration_hours: u32, ) -> Result { // Load farmer data let mut farmer_data = UserPersistence::load_user_data(farmer_email) .ok_or_else(|| "Farmer not found".to_string())?; // Find the node let node_index = farmer_data.nodes.iter().position(|n| n.id == node_id) .ok_or_else(|| "Node not found".to_string())?; let node = &mut farmer_data.nodes[node_index]; // Find the slice combination let combination = node.available_combinations.iter() .find(|c| c.get("id").and_then(|v| v.as_str()) == Some(combination_id)) .ok_or_else(|| "Slice combination not found".to_string())? .clone(); // Check availability let available_qty = combination.get("quantity_available").and_then(|v| v.as_u64()).unwrap_or(0) as u32; if available_qty < quantity { return Err(format!("Insufficient availability. Available: {}, Requested: {}", available_qty, quantity)); } // Calculate costs let base_slices_required = combination.get("base_slices_required").and_then(|v| v.as_u64()).unwrap_or(1) as u32; let total_base_slices_needed = base_slices_required * quantity; let price_per_hour = combination.get("price_per_hour") .and_then(|p| p.as_str()) .and_then(|p_str| rust_decimal::Decimal::from_str(p_str).ok()) .unwrap_or_else(|| Decimal::from_f64(1.0).unwrap_or_default()); let hourly_cost = price_per_hour * Decimal::from(quantity); let total_cost = hourly_cost * Decimal::from(rental_duration_hours); // Check renter's balance let mut renter_data = UserPersistence::load_user_data(renter_email) .unwrap_or_else(|| self.create_default_user_data(renter_email)); if renter_data.wallet_balance_usd < total_cost { return Err(format!("Insufficient balance. Required: ${}, Available: ${}", total_cost, renter_data.wallet_balance_usd)); } // Create allocation let allocation_id = format!("alloc_{}", &uuid::Uuid::new_v4().to_string()[..8]); let rental_id = format!("rental_{}", &uuid::Uuid::new_v4().to_string()[..8]); let allocation = SliceAllocation { allocation_id: allocation_id.clone(), slice_combination_id: combination_id.to_string(), renter_email: renter_email.to_string(), base_slices_used: total_base_slices_needed, rental_start: Utc::now(), rental_end: Some(Utc::now() + chrono::Duration::hours(rental_duration_hours as i64)), status: AllocationStatus::Active, monthly_cost: hourly_cost * Decimal::from(24 * 30), // Approximate monthly cost }; // Update node availability self.slice_calculator.update_availability_after_rental( node, total_base_slices_needed, farmer_email )?; // Add allocation to node node.slice_allocations.push(serde_json::to_value(allocation.clone()).unwrap_or_default()); // Create rental record let slice_rental = SliceRental { rental_id: rental_id.clone(), slice_combination_id: combination_id.to_string(), node_id: node_id.to_string(), farmer_email: farmer_email.to_string(), slice_allocation: allocation, total_cost, payment_status: PaymentStatus::Paid, slice_format: "standard".to_string(), user_email: renter_email.to_string(), status: "Active".to_string(), start_date: Some(chrono::Utc::now()), rental_duration_days: Some(30), monthly_cost: Some(total_cost), id: rental_id.clone(), deployment_type: None, deployment_name: None, deployment_config: None, deployment_status: None, deployment_endpoint: None, deployment_metadata: None, }; // Deduct payment from renter renter_data.wallet_balance_usd -= total_cost; renter_data.slice_rentals.push(slice_rental.clone()); // Add earnings to farmer farmer_data.wallet_balance_usd += total_cost; // Save both user data UserPersistence::save_user_data(&farmer_data) .map_err(|e| format!("Failed to save farmer data: {}", e))?; UserPersistence::save_user_data(&renter_data) .map_err(|e| format!("Failed to save renter data: {}", e))?; Ok(slice_rental) } /// Release expired slice rentals pub fn release_expired_rentals(&self, farmer_email: &str) -> Result { let mut farmer_data = UserPersistence::load_user_data(farmer_email) .ok_or_else(|| "Farmer not found".to_string())?; let mut released_count = 0; let now = Utc::now(); for node in &mut farmer_data.nodes { let mut expired_allocations = Vec::new(); // Find expired allocations for (index, allocation) in node.slice_allocations.iter().enumerate() { if let Some(end_time) = allocation.get("rental_end") .and_then(|r| r.as_str()) .and_then(|r_str| chrono::DateTime::parse_from_rfc3339(r_str).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)) { if now > end_time && allocation.get("status") .and_then(|s| s.as_str()) .map(|s| s == "Active") .unwrap_or(false) { expired_allocations.push(index); } } } // Remove expired allocations and update availability for &index in expired_allocations.iter().rev() { let base_slices_used = { let allocation = &mut node.slice_allocations[index]; if let Some(allocation_obj) = allocation.as_object_mut() { allocation_obj.insert("status".to_string(), serde_json::Value::String("Expired".to_string())); } allocation.get("base_slices_used") .and_then(|b| b.as_u64()) .unwrap_or(0) as u32 }; // Update availability self.slice_calculator.update_availability_after_release( node, base_slices_used, farmer_email )?; released_count += 1; } } if released_count > 0 { UserPersistence::save_user_data(&farmer_data) .map_err(|e| format!("Failed to save farmer data: {}", e))?; } Ok(released_count) } /// Get slice rental statistics for a farmer pub fn get_farmer_slice_statistics(&self, farmer_email: &str) -> SliceRentalStatistics { if let Some(farmer_data) = UserPersistence::load_user_data(farmer_email) { let mut stats = SliceRentalStatistics::default(); for node in &farmer_data.nodes { stats.total_nodes += 1; stats.total_base_slices += node.total_base_slices as u32; stats.allocated_base_slices += node.allocated_base_slices as u32; stats.active_rentals += node.slice_allocations.iter() .filter(|a| a.get("status") .and_then(|s| s.as_str()) .map(|s| s == "Active") .unwrap_or(false)) .count() as u32; // Calculate earnings from slice rentals for allocation in &node.slice_allocations { if allocation.get("status") .and_then(|s| s.as_str()) .map(|s| s == "Active") .unwrap_or(false) { let monthly_cost = allocation.get("monthly_cost") .and_then(|c| c.as_str()) .and_then(|c_str| rust_decimal::Decimal::from_str(c_str).ok()) .unwrap_or_default(); stats.monthly_earnings += monthly_cost; } } } stats.utilization_percentage = if stats.total_base_slices > 0 { (stats.allocated_base_slices as f64 / stats.total_base_slices as f64) * 100.0 } else { 0.0 }; stats } else { SliceRentalStatistics::default() } } /// Create default user data using centralized builder fn create_default_user_data(&self, user_email: &str) -> UserPersistentData { crate::models::builders::SessionDataBuilder::new_user(user_email) } } /// Statistics for farmer slice rentals #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SliceRentalStatistics { pub total_nodes: u32, pub total_base_slices: u32, pub allocated_base_slices: u32, pub active_rentals: u32, pub utilization_percentage: f64, pub monthly_earnings: Decimal, }