init projectmycelium
This commit is contained in:
323
src/services/node_rental.rs
Normal file
323
src/services/node_rental.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
//! 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user