This repository has been archived on 2025-12-01. You can view files and clone it, but cannot push or open issues or pull requests.
Files
projectmycelium_old/src/services/node_rental.rs
2025-09-01 21:37:01 -04:00

323 lines
12 KiB
Rust

//! 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<bool>,
notification_enabled: Option<bool>,
conflict_prevention: Option<bool>,
}
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<NodeRentalService, String> {
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<String, String> {
// 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<NodeRental> {
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<FarmerRentalEarning> {
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())
}
}
}