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, // Number of days for the rental } #[derive(Debug, Serialize)] pub struct RentalResponse { pub success: bool, pub message: String, pub rental_id: Option, pub transaction_id: Option, } impl RentalController { /// Rent a product pub async fn rent_product( product_id: web::Path, request: Option>, session: Session, ) -> Result { // Get user from session let user_email = match session.get::("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::("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, session: Session, ) -> Result { // 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::("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::("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, session: Session, ) -> Result { // 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::("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::("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, request: Option>, session: Session, ) -> Result { // 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::("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, }