323 lines
12 KiB
Rust
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())
|
|
}
|
|
}
|
|
} |