init projectmycelium

This commit is contained in:
mik-tf
2025-09-01 21:37:01 -04:00
commit b41efb0e99
319 changed files with 128160 additions and 0 deletions

642
src/controllers/rental.rs Normal file
View File

@@ -0,0 +1,642 @@
use actix_web::{web, Result, Responder};
use actix_session::Session;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::utils::response_builder::ResponseBuilder;
use crate::models::user::{User, Transaction, TransactionType, TransactionStatus};
use crate::config::get_app_config;
use crate::services::product::ProductService;
use crate::services::user_persistence::UserPersistence;
use chrono::Utc;
/// Controller for handling rental and purchase operations
pub struct RentalController;
#[derive(Debug, Deserialize)]
pub struct RentProductRequest {
pub product_id: String,
pub duration: String, // "monthly", "yearly", etc.
#[serde(default)]
pub duration_days: Option<u32>, // Number of days for the rental
}
#[derive(Debug, Serialize)]
pub struct RentalResponse {
pub success: bool,
pub message: String,
pub rental_id: Option<String>,
pub transaction_id: Option<String>,
}
impl RentalController {
/// Rent a product
pub async fn rent_product(
product_id: web::Path<String>,
request: Option<web::Json<RentProductRequest>>,
session: Session,
) -> Result<impl Responder> {
// Get user from session
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User not authenticated".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Get user data
let user_json = match session.get::<String>("user")? {
Some(json) => json,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User data not found".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
let mut user: User = match serde_json::from_str(&user_json) {
Ok(u) => u,
Err(_) => {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Invalid user data".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Get product from ProductService
let product_service = ProductService::new();
let product = match product_service.get_product_by_id(&product_id) {
Some(p) => p,
None => {
return ResponseBuilder::not_found().json(RentalResponse {
success: false,
message: "Product not found".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Load user persistent data and check if product is already rented
let mut user_data = UserPersistence::load_user_data(&user_email)
.unwrap_or_else(|| UserPersistence::create_default_user_data(&user_email));
// Check if product is already in active rentals
let already_rented = user_data.slice_rentals.iter()
.any(|rental| rental.slice_format == product_id.to_string());
if already_rented {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Product already rented by user".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
// Check user balance from persistent data
let user_balance = user_data.wallet_balance_usd;
let rental_cost = product.base_price;
if user_balance < rental_cost {
let required = rental_cost;
let available = user_balance;
let deficit = required - available;
return ResponseBuilder::payment_required()
.error_envelope(
"INSUFFICIENT_FUNDS",
"Insufficient balance",
serde_json::json!({
"currency": "USD",
"wallet_balance_usd": available,
"required_usd": required,
"deficit_usd": deficit
})
)
.build();
}
// Extract request body (required when mocks enabled)
let req_data = match request {
Some(r) => r.into_inner(),
None => {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Missing or invalid request body".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Create rental and transaction
let rental_id = uuid::Uuid::new_v4().to_string();
let transaction_id = uuid::Uuid::new_v4().to_string();
let transaction = Transaction {
id: transaction_id.clone(),
user_id: user_email.clone(),
transaction_type: TransactionType::Rental {
rental_id: transaction_id.clone(),
rental_type: "slice".to_string(),
},
amount: rental_cost,
currency: Some("USD".to_string()),
exchange_rate_usd: Some(rust_decimal::Decimal::ONE),
amount_usd: Some(rental_cost),
description: Some(format!("Rental of product {} for {}", product_id, req_data.duration)),
reference_id: Some(format!("rental-{}", uuid::Uuid::new_v4())),
metadata: None,
timestamp: Utc::now(),
status: TransactionStatus::Completed,
};
// Update user persistent data
// Deduct balance
user_data.wallet_balance_usd -= rental_cost;
// Add transaction
user_data.transactions.push(transaction);
// Create a slice rental record
let slice_rental = crate::services::slice_calculator::SliceRental {
rental_id: rental_id.clone(),
slice_combination_id: format!("combo-{}", rental_id),
node_id: "node-placeholder".to_string(), // TODO: Get from product
farmer_email: "farmer@example.com".to_string(), // TODO: Get from product
slice_allocation: crate::services::slice_calculator::SliceAllocation {
allocation_id: format!("alloc-{}", rental_id),
slice_combination_id: format!("combo-{}", rental_id),
renter_email: user_email.clone(),
base_slices_used: 1,
rental_start: Utc::now(),
rental_end: None,
status: crate::services::slice_calculator::AllocationStatus::Active,
monthly_cost: rental_cost,
},
total_cost: rental_cost,
payment_status: crate::services::slice_calculator::PaymentStatus::Paid,
id: rental_id.clone(),
user_email: user_email.clone(),
slice_format: "1x1".to_string(),
status: "Active".to_string(),
start_date: Some(Utc::now()),
rental_duration_days: Some(30),
monthly_cost: Some(rental_cost),
deployment_type: Some("vm".to_string()),
deployment_name: Some(format!("deployment-{}", rental_id)),
deployment_config: None,
deployment_status: Some("Provisioning".to_string()),
deployment_endpoint: None,
deployment_metadata: None,
};
user_data.slice_rentals.push(slice_rental);
// Add user activity
user_data.user_activities.push(crate::models::user::UserActivity {
id: Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::SliceRental,
description: format!("Rented {} for ${}", product.name, rental_cost),
metadata: Some(serde_json::json!({
"product_id": product_id.to_string(),
"rental_id": rental_id,
"cost": rental_cost
})),
timestamp: Utc::now(),
ip_address: None,
user_agent: None,
session_id: None,
category: "Rental".to_string(),
importance: crate::models::user::ActivityImportance::Medium,
});
// Save updated user data
UserPersistence::save_user_data(&user_data)?;
ResponseBuilder::ok().json(RentalResponse {
success: true,
message: format!("Successfully rented {} for ${}", product.name, rental_cost),
rental_id: Some(rental_id),
transaction_id: Some(transaction_id),
}).build()
}
/// Purchase a product (one-time payment)
pub async fn purchase_product(
product_id: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
// Gate mock-based purchase when mocks are disabled
if !get_app_config().enable_mock_data() {
return ResponseBuilder::not_found().json(RentalResponse {
success: false,
message: "Purchase feature unavailable".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
// Get user from session
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User not authenticated".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Get user data
let user_json = match session.get::<String>("user")? {
Some(json) => json,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User data not found".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
let mut user: User = match serde_json::from_str(&user_json) {
Ok(u) => u,
Err(_) => {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Invalid user data".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Get product from ProductService (replaces MockDataService)
let product_service = match crate::services::product::ProductService::builder().build() {
Ok(service) => service,
Err(_) => {
return ResponseBuilder::internal_error().json(RentalResponse {
success: false,
message: "Failed to initialize product service".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
let product = match product_service.get_product_by_id(&product_id) {
Some(p) => p,
None => {
return ResponseBuilder::not_found().json(RentalResponse {
success: false,
message: "Product not found".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Check if product is already owned by this user
if let Ok(owned_products) = user.get_owned_products() {
if owned_products.iter().any(|p| p.id == **product_id) {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Product already owned by user".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
}
// Continue with rental logic if not owned
if false { // This condition will be replaced by the existing logic below
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Product already owned by user".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
// Check user balance
let user_balance = user.get_wallet_balance()?;
let purchase_cost = product.base_price;
if user_balance < purchase_cost {
let required = purchase_cost;
let available = user_balance;
let deficit = required - available;
return ResponseBuilder::payment_required()
.error_envelope(
"INSUFFICIENT_FUNDS",
"Insufficient balance",
serde_json::json!({
"currency": "USD",
"wallet_balance_usd": available,
"required_usd": required,
"deficit_usd": deficit
})
)
.build();
}
// Create transaction
let transaction_id = uuid::Uuid::new_v4().to_string();
let transaction = Transaction {
id: transaction_id.clone(),
user_id: user_email.clone(),
transaction_type: TransactionType::Purchase {
product_id: product_id.to_string(),
},
amount: purchase_cost,
currency: Some("USD".to_string()),
exchange_rate_usd: Some(rust_decimal::Decimal::ONE),
amount_usd: Some(purchase_cost),
description: Some(format!("Purchase of product {}", product_id)),
reference_id: Some(format!("purchase-{}", uuid::Uuid::new_v4())),
metadata: None,
timestamp: Utc::now(),
status: TransactionStatus::Completed,
};
// Update user data using persistent data
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(&user_email);
// Deduct balance
persistent_data.wallet_balance_usd -= purchase_cost;
// Add to owned products
persistent_data.owned_product_ids.push(product_id.to_string());
// Add transaction
persistent_data.transactions.push(transaction);
// Update user activities
persistent_data.user_activities.insert(0, crate::models::user::UserActivity {
id: uuid::Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::Purchase,
description: format!("Purchased {} for ${}", product.name, purchase_cost),
timestamp: Utc::now(),
metadata: None,
category: "Purchase".to_string(),
importance: crate::models::user::ActivityImportance::High,
ip_address: None,
user_agent: None,
session_id: None,
});
// Save the updated persistent data
if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data(&persistent_data) {
log::error!("Failed to save user data after purchase: {}", e);
}
// Update session with new user data
let updated_user_json = serde_json::to_string(&user).unwrap();
session.insert("user", updated_user_json)?;
ResponseBuilder::ok().json(RentalResponse {
success: true,
message: format!("Successfully purchased {} for ${}", product.name, purchase_cost),
rental_id: None,
transaction_id: Some(transaction_id),
}).build()
}
/// Cancel a rental
pub async fn cancel_rental(
rental_id: web::Path<String>,
session: Session,
) -> Result<impl Responder> {
// Gate mock-based rental cancel when mocks are disabled
if !get_app_config().enable_mock_data() {
return ResponseBuilder::not_found().json(RentalResponse {
success: false,
message: "Rental feature unavailable".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
// Get user from session
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User not authenticated".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Get user data
let user_json = match session.get::<String>("user")? {
Some(json) => json,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User data not found".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
let mut user: User = match serde_json::from_str(&user_json) {
Ok(u) => u,
Err(_) => {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Invalid user data".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Update user data using persistent data
let user_email = &user.email;
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
// Remove rental
if let Some(pos) = persistent_data.active_product_rentals.iter().position(|x| x.rental_id == rental_id.to_string()) {
persistent_data.active_product_rentals.remove(pos);
// Update user activities
persistent_data.user_activities.insert(0, crate::models::user::UserActivity {
id: uuid::Uuid::new_v4().to_string(),
user_email: user_email.clone(),
activity_type: crate::models::user::ActivityType::Purchase,
description: format!("Cancelled rental {}", rental_id),
timestamp: Utc::now(),
metadata: None,
category: "Rental".to_string(),
importance: crate::models::user::ActivityImportance::Medium,
ip_address: None,
user_agent: None,
session_id: None,
});
// Keep only last 10 activities
if persistent_data.user_activities.len() > 10 {
persistent_data.user_activities.truncate(10);
}
// Save the updated persistent data
if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data(&persistent_data) {
log::error!("Failed to save user data after rental cancellation: {}", e);
}
} else {
return ResponseBuilder::not_found().json(RentalResponse {
success: false,
message: "Rental not found".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
// Update session with new user data
let updated_user_json = serde_json::to_string(&user).unwrap();
session.insert("user", updated_user_json)?;
ResponseBuilder::ok().json(RentalResponse {
success: true,
message: "Rental cancelled successfully".to_string(),
rental_id: Some(rental_id.to_string()),
transaction_id: None,
}).build()
}
/// Rent a node product (slice or full node)
pub async fn rent_node_product(
product_id: web::Path<String>,
request: Option<web::Json<RentNodeProductRequest>>,
session: Session,
) -> Result<impl Responder> {
// Gate mock-based node rental when mocks are disabled
if !get_app_config().enable_mock_data() {
return ResponseBuilder::not_found().json(RentalResponse {
success: false,
message: "Node rental feature unavailable".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
// Get user from session
let user_email = match session.get::<String>("user_email")? {
Some(email) => email,
None => {
return ResponseBuilder::unauthorized().json(RentalResponse {
success: false,
message: "User not authenticated".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Initialize node rental service
let node_rental_service = match crate::services::node_rental::NodeRentalService::builder()
.auto_billing_enabled(true)
.notification_enabled(true)
.conflict_prevention(true)
.build()
{
Ok(service) => service,
Err(e) => {
return ResponseBuilder::internal_error().json(RentalResponse {
success: false,
message: format!("Service initialization failed: {}", e),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Extract request body (required when mocks enabled)
let req_data = match request {
Some(r) => r.into_inner(),
None => {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Missing or invalid request body".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Parse duration and rental type
let duration_months = match req_data.duration.as_str() {
"monthly" => 1,
"quarterly" => 3,
"yearly" => 12,
_ => {
return ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: "Invalid duration. Use 'monthly', 'quarterly', or 'yearly'".to_string(),
rental_id: None,
transaction_id: None,
}).build();
}
};
// Determine rental type and cost based on product
let (rental_type, monthly_cost) = if product_id.starts_with("fullnode_") {
(crate::models::user::NodeRentalType::FullNode, req_data.monthly_cost.unwrap_or_else(|| rust_decimal::Decimal::from(200)))
} else {
// For slice products, we'd need to get slice configuration
// For now, use a default slice configuration
(crate::models::user::NodeRentalType::Slice, req_data.monthly_cost.unwrap_or_else(|| rust_decimal::Decimal::from(50)))
};
// Attempt to rent the node
match node_rental_service.rent_node_product(
&product_id,
&user_email,
duration_months,
rental_type,
monthly_cost,
) {
Ok((rental, _earning)) => {
ResponseBuilder::ok().json(RentalResponse {
success: true,
message: "Node rental successful".to_string(),
rental_id: Some(rental.id),
transaction_id: None,
}).build()
}
Err(e) => {
ResponseBuilder::bad_request().json(RentalResponse {
success: false,
message: e,
rental_id: None,
transaction_id: None,
}).build()
}
}
}
}
#[derive(Debug, serde::Deserialize)]
pub struct RentNodeProductRequest {
pub duration: String, // "monthly", "quarterly", "yearly"
pub monthly_cost: Option<rust_decimal::Decimal>,
}