642 lines
24 KiB
Rust
642 lines
24 KiB
Rust
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>,
|
|
} |