//! Node rental service for managing node rentals and farmer earnings //! Follows the established builder pattern for consistent API design use crate::models::user::{NodeRental, NodeRentalType, NodeRentalStatus, FarmerRentalEarning, PaymentStatus, NodeAvailabilityStatus}; use crate::services::user_persistence::{UserPersistence, ProductRental}; use rust_decimal::Decimal; use chrono::{Utc, Duration}; use std::collections::HashMap; /// Service for node rental operations #[derive(Clone)] pub struct NodeRentalService { auto_billing_enabled: bool, notification_enabled: bool, conflict_prevention: bool, } /// Builder for NodeRentalService #[derive(Default)] pub struct NodeRentalServiceBuilder { auto_billing_enabled: Option, notification_enabled: Option, conflict_prevention: Option, } impl NodeRentalServiceBuilder { pub fn new() -> Self { Self::default() } pub fn auto_billing_enabled(mut self, enabled: bool) -> Self { self.auto_billing_enabled = Some(enabled); self } pub fn notification_enabled(mut self, enabled: bool) -> Self { self.notification_enabled = Some(enabled); self } pub fn conflict_prevention(mut self, enabled: bool) -> Self { self.conflict_prevention = Some(enabled); self } pub fn build(self) -> Result { Ok(NodeRentalService { auto_billing_enabled: self.auto_billing_enabled.unwrap_or(true), notification_enabled: self.notification_enabled.unwrap_or(true), conflict_prevention: self.conflict_prevention.unwrap_or(true), }) } } impl NodeRentalService { pub fn builder() -> NodeRentalServiceBuilder { NodeRentalServiceBuilder::new() } /// Rent a node product (slice or full node) pub fn rent_node_product( &self, product_id: &str, renter_email: &str, duration_months: u32, rental_type: NodeRentalType, monthly_cost: Decimal, ) -> Result<(NodeRental, FarmerRentalEarning), String> { // Extract node ID from product ID let node_id = if product_id.starts_with("fullnode_") { product_id.strip_prefix("fullnode_").unwrap_or(product_id) } else if product_id.starts_with("slice_") { // For slice products, we need to find the associated node // This would typically come from the product metadata product_id.strip_prefix("slice_").unwrap_or(product_id) } else { product_id }; // Check for conflicts if enabled if self.conflict_prevention { self.check_rental_conflicts(node_id, &rental_type)?; } // Calculate rental period let start_date = Utc::now(); let end_date = start_date + Duration::days((duration_months * 30) as i64); // Create rental record let rental = crate::models::builders::NodeRentalBuilder::new() .node_id(node_id.to_string()) .renter_email(renter_email.to_string()) .rental_type(rental_type.clone()) .monthly_cost(monthly_cost) .start_date(start_date) .end_date(end_date) .status(NodeRentalStatus::Active) .auto_renewal(false) .payment_method("USD".to_string()) .build()?; // Create farmer earning record let farmer_earning = crate::models::builders::FarmerRentalEarningBuilder::new() .node_id(node_id.to_string()) .rental_id(rental.id.clone()) .renter_email(renter_email.to_string()) .amount(monthly_cost) .currency("USD".to_string()) .earning_date(start_date) .rental_type(rental_type) .payment_status(PaymentStatus::Completed) .build()?; // Find the farmer who owns this node let farmer_email = self.find_node_owner(node_id)?; // Save rental to renter's data self.save_rental_to_user(&rental, renter_email, product_id)?; // Save earning to farmer's data self.save_earning_to_farmer(&farmer_earning, &farmer_email)?; // Update node availability status self.update_node_availability(node_id, &farmer_email)?; Ok((rental, farmer_earning)) } /// Check for rental conflicts fn check_rental_conflicts(&self, node_id: &str, rental_type: &NodeRentalType) -> Result<(), String> { // Find the farmer who owns this node let farmer_email = self.find_node_owner(node_id)?; if let Some(farmer_data) = UserPersistence::load_user_data(&farmer_email) { // Check existing rentals for this node let existing_rentals: Vec<_> = farmer_data.node_rentals.iter() .filter(|r| r.node_id == node_id && r.is_active()) .collect(); for existing_rental in existing_rentals { match (&existing_rental.rental_type, rental_type) { (NodeRentalType::FullNode, _) => { return Err("Cannot rent: full node is currently rented".to_string()); } (_, NodeRentalType::FullNode) => { return Err("Cannot rent full node: slices are currently rented".to_string()); } (NodeRentalType::Slice, NodeRentalType::Slice) => { // Check if there's enough capacity for additional slices // This would require more complex capacity tracking // For now, we'll allow multiple slice rentals } (NodeRentalType::SliceRental, NodeRentalType::SliceRental) => { // Allow multiple slice rentals } (NodeRentalType::SliceRental, NodeRentalType::Slice) => { // Allow slice rental when slice rental exists } (NodeRentalType::Slice, NodeRentalType::SliceRental) => { // Allow slice rental when slice exists } } } } Ok(()) } /// Find the farmer who owns a specific node fn find_node_owner(&self, node_id: &str) -> Result { // Scan all user files to find the node owner if let Ok(entries) = std::fs::read_dir("./user_data/") { for entry in entries.flatten() { if let Some(filename) = entry.file_name().to_str() { if filename.ends_with(".json") && filename.contains("_at_") && !filename.contains("_cart") && filename != "session_data.json" { let user_email = filename .trim_end_matches(".json") .replace("_at_", "@") .replace("_", "."); if let Some(user_data) = UserPersistence::load_user_data(&user_email) { if user_data.nodes.iter().any(|node| node.id == node_id) { return Ok(user_email); } } } } } } Err(format!("Node owner not found for node: {}", node_id)) } /// Save rental record to user's persistent data fn save_rental_to_user(&self, rental: &NodeRental, user_email: &str, product_id: &str) -> Result<(), String> { let mut user_data = UserPersistence::load_user_data(user_email) .unwrap_or_else(|| self.create_default_user_data(user_email)); // Add to node rentals user_data.node_rentals.push(rental.clone()); // Add to product rentals for dashboard display let product_rental = ProductRental { id: rental.id.clone(), product_id: product_id.to_string(), product_name: format!("Node Rental {}", product_id), rental_type: "node".to_string(), customer_email: user_email.to_string(), provider_email: "unknown@provider.com".to_string(), // TODO: Get from actual provider monthly_cost: rental.monthly_cost, status: "Active".to_string(), rental_id: rental.id.clone(), start_date: rental.start_date.to_rfc3339(), end_date: rental.end_date.to_rfc3339(), metadata: std::collections::HashMap::new(), }; user_data.active_product_rentals.push(product_rental); UserPersistence::save_user_data(&user_data) .map_err(|e| format!("Failed to save user data: {}", e))?; Ok(()) } /// Save earning record to farmer's persistent data fn save_earning_to_farmer(&self, earning: &FarmerRentalEarning, farmer_email: &str) -> Result<(), String> { let mut farmer_data = UserPersistence::load_user_data(farmer_email) .unwrap_or_else(|| self.create_default_user_data(farmer_email)); // Add to farmer rental earnings farmer_data.farmer_rental_earnings.push(earning.clone()); // Update wallet balance farmer_data.wallet_balance_usd += earning.amount; UserPersistence::save_user_data(&farmer_data) .map_err(|e| format!("Failed to save farmer data: {}", e))?; Ok(()) } /// Update node availability status based on current rentals fn update_node_availability(&self, node_id: &str, farmer_email: &str) -> Result<(), String> { let mut farmer_data = UserPersistence::load_user_data(farmer_email) .ok_or("Farmer data not found")?; if let Some(node) = farmer_data.nodes.iter_mut().find(|n| n.id == node_id) { // Count active rentals for this node let active_rentals: Vec<_> = farmer_data.node_rentals.iter() .filter(|r| r.node_id == node_id && r.is_active()) .collect(); node.availability_status = if active_rentals.is_empty() { NodeAvailabilityStatus::Available } else if active_rentals.iter().any(|r| matches!(r.rental_type, NodeRentalType::FullNode)) { NodeAvailabilityStatus::FullyRented } else { NodeAvailabilityStatus::PartiallyRented }; UserPersistence::save_user_data(&farmer_data) .map_err(|e| format!("Failed to update node availability: {}", e))?; } Ok(()) } /// Create default user data structure using centralized builder fn create_default_user_data(&self, user_email: &str) -> crate::services::user_persistence::UserPersistentData { crate::models::builders::SessionDataBuilder::new_user(user_email) } /// Get active rentals for a user pub fn get_user_rentals(&self, user_email: &str) -> Vec { if let Some(user_data) = UserPersistence::load_user_data(user_email) { user_data.node_rentals.into_iter() .filter(|r| r.is_active()) .collect() } else { Vec::new() } } /// Get farmer earnings from rentals pub fn get_farmer_rental_earnings(&self, farmer_email: &str) -> Vec { if let Some(farmer_data) = UserPersistence::load_user_data(farmer_email) { farmer_data.farmer_rental_earnings } else { Vec::new() } } /// Cancel a rental pub fn cancel_rental(&self, rental_id: &str, user_email: &str) -> Result<(), String> { let mut user_data = UserPersistence::load_user_data(user_email) .ok_or("User data not found")?; // Find and update the rental if let Some(rental) = user_data.node_rentals.iter_mut().find(|r| r.id == rental_id) { rental.status = NodeRentalStatus::Cancelled; // Update product rental status if let Some(product_rental) = user_data.active_product_rentals.iter_mut().find(|pr| pr.id == rental_id) { product_rental.status = "Cancelled".to_string(); } // Update node availability let farmer_email = self.find_node_owner(&rental.node_id)?; self.update_node_availability(&rental.node_id, &farmer_email)?; UserPersistence::save_user_data(&user_data) .map_err(|e| format!("Failed to save user data: {}", e))?; Ok(()) } else { Err("Rental not found".to_string()) } } }