use actix_web::{web, Result, Responder}; use tera::Tera; use crate::utils::render_template; use crate::utils::response_builder::ResponseBuilder; use crate::config::get_app_config; use crate::services::{currency::CurrencyService, product::ProductService, node_marketplace::NodeMarketplaceService, slice_rental::SliceRentalService, slice_assignment::{SliceAssignmentService, SliceAssignmentRequest, DeploymentConfiguration, AssignmentMode, NetworkConfiguration, SecurityConfiguration, VMConfiguration}}; use crate::services::slice_assignment::DeploymentType as SliceDeploymentType; use actix_session::Session; use serde::{Deserialize, Serialize}; use uuid::Uuid; use chrono::Utc; /// Form data for slice rental requests #[derive(Debug, Deserialize)] pub struct SliceRentalForm { pub farmer_email: String, pub node_id: String, pub combination_id: String, pub quantity: u32, pub rental_duration_hours: u32, pub deployment_type: String, // "vm" or "kubernetes" // VM-specific options pub vm_name: Option, pub vm_os: Option, // "ubuntu", "debian", "centos", "alpine" pub vm_ssh_key: Option, // Kubernetes-specific options pub k8s_masters: Option, pub k8s_workers: Option, pub cluster_name: Option, pub k8s_version: Option, // "1.28", "1.29", "1.30" pub k8s_network_plugin: Option, // "flannel", "calico", "weave" // Common deployment options pub auto_scaling: Option, pub backup_enabled: Option, pub monitoring_enabled: Option, } /// Deployment configuration for slice rentals #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SliceDeploymentConfig { pub deployment_id: String, pub deployment_type: DeploymentType, pub slice_specs: SliceSpecs, pub network_config: NetworkConfig, pub security_config: SecurityConfig, pub created_at: chrono::DateTime, pub status: DeploymentStatus, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DeploymentType { VM { name: String, os: String, ssh_key: Option, }, Kubernetes { cluster_name: String, masters: u32, workers: u32, version: String, network_plugin: String, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SliceSpecs { pub cpu_cores: u32, pub memory_gb: u32, pub storage_gb: u32, pub quantity: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkConfig { pub public_ip: bool, pub private_network: String, pub ports: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecurityConfig { pub firewall_enabled: bool, pub ssh_access: bool, pub monitoring: bool, pub backup: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DeploymentStatus { Pending, Deploying, Running, Stopped, Failed, Terminated, } /// Controller for handling marketplace-related routes pub struct MarketplaceController; impl MarketplaceController { /// Renders the marketplace dashboard page pub async fn dashboard(tmpl: web::Data, session: Session) -> Result { let product_service = ProductService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let currency_service = CurrencyService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "dashboard"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Get user's preferred currency let display_currency = currency_service.get_user_preferred_currency(&session); // Get marketplace data (gate mock usage) // Get marketplace configuration let mut marketplace_config = crate::models::marketplace::MarketplaceConfig::default(); marketplace_config.marketplace.default_display_currency = display_currency.clone(); let featured_products = product_service.get_featured_products(); let categories = product_service.get_categories(); let product_stats = product_service.get_product_statistics(); // Get popular applications (first 3 application products) let app_criteria = crate::services::product::ProductSearchCriteria::new() .with_category("application".to_string()); let app_search_result = product_service.search_products_advanced(&app_criteria, 0, 3); // Get available services (first 3 service products) let service_criteria = crate::services::product::ProductSearchCriteria::new() .with_category("service".to_string()); let service_search_result = product_service.search_products_advanced(&service_criteria, 0, 3); // De-duplicate: exclude any application already featured let featured_ids: std::collections::HashSet = featured_products.iter().map(|p| p.id.clone()).collect(); let app_products_dedup: Vec<_> = app_search_result .products .into_iter() .filter(|p| !featured_ids.contains(&p.id)) .collect(); // Convert featured product prices to user's currency let featured_with_prices = match product_service.get_products_with_converted_prices( &featured_products, &display_currency, ) { Ok(converted) => converted.into_iter() .map(|(product, price)| { let formatted_price = currency_service.format_price( price.display_amount, &price.display_currency, ).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency)); serde_json::json!({ "product": product, "price": price, "formatted_price": formatted_price }) }) .collect::>(), Err(_) => Vec::new(), }; // Convert application prices to user's currency let applications_with_prices = match product_service.get_products_with_converted_prices( &app_products_dedup, &display_currency, ) { Ok(converted) => converted.into_iter() .map(|(product, price)| { let formatted_price = currency_service.format_price( price.display_amount, &price.display_currency, ).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency)); serde_json::json!({ "product": product, "price": price, "formatted_price": formatted_price }) }) .collect::>(), Err(_) => Vec::new(), }; // Convert service prices to user's currency let services_with_prices = match product_service.get_products_with_converted_prices( &service_search_result.products, &display_currency, ) { Ok(converted) => converted.into_iter() .map(|(product, price)| { let formatted_price = currency_service.format_price( price.display_amount, &price.display_currency, ).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency)); serde_json::json!({ "product": product, "price": price, "formatted_price": formatted_price }) }) .collect::>(), Err(_) => Vec::new(), }; // Add marketplace data to context ctx.insert("marketplace_config", &marketplace_config); ctx.insert("featured_products", &featured_with_prices); ctx.insert("popular_applications", &applications_with_prices); ctx.insert("available_services", &services_with_prices); ctx.insert("categories", &categories); ctx.insert("product_stats", &product_stats); ctx.insert("currencies", ¤cy_service.get_currency_display_info()); ctx.insert("user_currency", &display_currency); // Add user to context if available if let Ok(Some(user_json)) = session.get::("user") { // Keep the raw JSON for backward compatibility ctx.insert("user_json", &user_json); // Parse the JSON into a User object match serde_json::from_str::(&user_json) { Ok(user) => { ctx.insert("user", &user); }, Err(e) => { } } } render_template(&tmpl, "marketplace/dashboard.html", &ctx) } /// Renders the compute resources (slices) marketplace page pub async fn compute_resources(tmpl: web::Data, session: Session, query: web::Query>) -> Result { let currency_service = CurrencyService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let node_marketplace_service = NodeMarketplaceService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "compute_resources"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Get user's preferred currency let display_currency = currency_service.get_user_preferred_currency(&session); // Get all slice combinations from the marketplace let all_slice_products = node_marketplace_service.get_all_slice_combinations(); // Log first few products for debugging for (i, product) in all_slice_products.iter().take(3).enumerate() { } // Apply slice-specific filters let filtered_products = node_marketplace_service.apply_slice_filters(&all_slice_products, &query); // Parse pagination parameters let page = query.get("page") .and_then(|p| p.parse::().ok()) .unwrap_or(0); let page_size = 12; // Products per page // Apply pagination let total_count = filtered_products.len(); let total_pages = (total_count + page_size - 1) / page_size; let start_index = page * page_size; let end_index = std::cmp::min(start_index + page_size, total_count); let paginated_products = if start_index < total_count { filtered_products[start_index..end_index].to_vec() } else { Vec::new() }; // Convert prices to user's currency and format for display let products_with_prices = paginated_products.into_iter() .map(|product| { // Convert price to user's preferred currency using create_price let converted_price = currency_service.create_price( product.base_price, &product.base_currency, &display_currency, ).unwrap_or_else(|_| crate::models::currency::Price { base_amount: product.base_price, base_currency: product.base_currency.clone(), display_currency: display_currency.clone(), display_amount: product.base_price, formatted_display: format!("{} {}", product.base_price, product.base_currency), conversion_rate: rust_decimal::Decimal::from(1), conversion_timestamp: chrono::Utc::now(), }); let formatted_price = currency_service.format_price( converted_price.display_amount, &converted_price.display_currency, ).unwrap_or_else(|_| format!("{} {}", converted_price.display_amount, converted_price.display_currency)); serde_json::json!({ "product": product, "price": converted_price, "formatted_price": formatted_price }) }) .collect::>(); ctx.insert("compute_products", &products_with_prices); ctx.insert("slice_products", &products_with_prices); // Keep both for compatibility ctx.insert("currencies", ¤cy_service.get_currency_display_info()); ctx.insert("user_currency", &display_currency); // Add pagination data ctx.insert("pagination", &serde_json::json!({ "current_page": page, "total_pages": total_pages, "total_count": total_count, "page_size": page_size, "has_previous": page > 0, "has_next": page < total_pages.saturating_sub(1), "previous_page": page.saturating_sub(1), "next_page": page + 1 })); // Add slice marketplace statistics let slice_stats = node_marketplace_service.get_slice_marketplace_statistics(); ctx.insert("slice_statistics", &slice_stats); // Add available filter options let available_locations: Vec = all_slice_products.iter() .filter_map(|p| p.attributes.get("location")) .filter_map(|l| l.value.as_str()) .map(|l| l.to_string()) .collect::>() .into_iter() .collect(); ctx.insert("available_locations", &available_locations); let available_certifications: Vec = all_slice_products.iter() .filter_map(|p| p.attributes.get("node_characteristics")) .filter_map(|chars| chars.value.get("certification_type")) .filter_map(|cert| cert.as_str()) .map(|cert| cert.to_string()) .collect::>() .into_iter() .collect(); ctx.insert("available_certifications", &available_certifications); // Add current filters to context ctx.insert("current_filters", &query.into_inner()); // Add user data to context if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); if let Ok(user) = serde_json::from_str::(&user_json) { ctx.insert("user", &user); } } render_template(&tmpl, "marketplace/compute_resources.html", &ctx) } /// Renders the Mycelium Nodes marketplace page with REAL farmer nodes from database pub async fn mycelium_nodes(tmpl: web::Data, session: Session, query: web::Query>) -> Result { // Build services using established builder pattern let currency_service = CurrencyService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let node_marketplace_service = NodeMarketplaceService::builder() .currency_service(currency_service.clone()) .include_offline_nodes(false) // Only show online nodes in marketplace .price_calculation_method("capacity_based") .cache_enabled(true) .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Build context using ContextBuilder pattern let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .active_section("mycelium_nodes") .user_if_available(&session) .build(); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Get user's preferred currency let display_currency = currency_service.get_user_preferred_currency(&session); // Parse pagination parameters let page = query.get("page") .and_then(|p| p.parse::().ok()) .unwrap_or(0); let page_size = 12; // Get all real farmer nodes as marketplace products let all_node_products = node_marketplace_service.get_all_marketplace_nodes(); // Clone query for reuse let query_params = query.into_inner(); // Apply filters using the service's filter method let filtered_products = node_marketplace_service.apply_marketplace_filters(&all_node_products, &query_params); // Apply pagination let total_count = filtered_products.len(); let total_pages = (total_count + page_size - 1) / page_size; let start_idx = page * page_size; let end_idx = std::cmp::min(start_idx + page_size, total_count); let paginated_products = if start_idx < filtered_products.len() { filtered_products[start_idx..end_idx].to_vec() } else { Vec::new() }; // Convert prices to user's currency let products_with_prices = paginated_products.into_iter() .map(|product| { // Convert price to user's preferred currency using create_price let converted_price = currency_service.create_price( product.base_price, &product.base_currency, &display_currency, ).unwrap_or_else(|_| crate::models::currency::Price { base_amount: product.base_price, base_currency: product.base_currency.clone(), display_currency: display_currency.clone(), display_amount: product.base_price, formatted_display: format!("{} {}", product.base_price, product.base_currency), conversion_rate: rust_decimal::Decimal::from(1), conversion_timestamp: chrono::Utc::now(), }); let formatted_price = currency_service.format_price( converted_price.display_amount, &converted_price.display_currency, ).unwrap_or_else(|_| format!("{} {}", converted_price.display_amount, converted_price.display_currency)); serde_json::json!({ "product": product, "price": converted_price, "formatted_price": formatted_price }) }) .collect::>(); // Add data to context ctx.insert("hardware_products", &products_with_prices); ctx.insert("currencies", ¤cy_service.get_currency_display_info()); ctx.insert("user_currency", &display_currency); ctx.insert("current_filters", &query_params); // Add pagination data ctx.insert("pagination", &serde_json::json!({ "current_page": page, "total_pages": total_pages, "total_count": total_count, "page_size": page_size, "has_previous": page > 0, "has_next": page < total_pages.saturating_sub(1), "previous_page": page.saturating_sub(1), "next_page": page + 1 })); // Add node marketplace statistics ctx.insert("node_statistics", &node_marketplace_service.get_capacity_statistics()); ctx.insert("available_regions", &node_marketplace_service.get_available_regions()); render_template(&tmpl, "marketplace/mycelium_nodes.html", &ctx) } /// Renders the gateways marketplace page pub async fn gateways(tmpl: web::Data, session: Session, query: web::Query>) -> Result { let product_service = ProductService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let currency_service = CurrencyService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "gateways"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Get user's preferred currency let display_currency = currency_service.get_user_preferred_currency(&session); // Parse pagination and filter parameters let page = query.get("page") .and_then(|p| p.parse::().ok()) .unwrap_or(0); let page_size = 12; // Products per page // Build search criteria from query parameters let mut criteria = crate::services::product::ProductSearchCriteria::new() .with_category("gateway".to_string()); // Add filters from query parameters if let Some(location) = query.get("location") { if !location.is_empty() { criteria = criteria.with_location(location.clone()); } } if let Some(bandwidth) = query.get("bandwidth_mbps") { if let Ok(bw) = bandwidth.parse::() { criteria = criteria.with_attribute("bandwidth_mbps".to_string(), serde_json::Value::Number(serde_json::Number::from(bw))); } } let mut min_price_val = None; let mut max_price_val = None; if let Some(min_price) = query.get("min_price") { if let Ok(price) = min_price.parse::() { min_price_val = Some(price); } } if let Some(max_price) = query.get("max_price") { if let Ok(price) = max_price.parse::() { max_price_val = Some(price); } } if min_price_val.is_some() || max_price_val.is_some() { criteria = criteria.with_price_range(min_price_val, max_price_val); } // Search products with pagination let search_result = product_service.search_products_advanced(&criteria, page, page_size); // Convert prices to user's currency let products_with_prices = match product_service.get_products_with_converted_prices( &search_result.products, &display_currency, ) { Ok(converted) => converted.into_iter() .map(|(product, price)| { let formatted_price = currency_service.format_price( price.display_amount, &price.display_currency, ).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency)); serde_json::json!({ "product": product, "price": price, "formatted_price": formatted_price }) }) .collect::>(), Err(_) => Vec::new(), }; ctx.insert("gateway_products", &products_with_prices); ctx.insert("currencies", ¤cy_service.get_currency_display_info()); ctx.insert("user_currency", &display_currency); // Add pagination data ctx.insert("pagination", &serde_json::json!({ "current_page": search_result.page, "total_pages": search_result.total_pages, "total_count": search_result.total_count, "page_size": search_result.page_size, "has_previous": search_result.page > 0, "has_next": search_result.page < search_result.total_pages.saturating_sub(1), "previous_page": search_result.page.saturating_sub(1), "next_page": search_result.page + 1 })); // Add current filters to context ctx.insert("current_filters", &query.into_inner()); // Add user data to context if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); if let Ok(user) = serde_json::from_str::(&user_json) { ctx.insert("user", &user); } } render_template(&tmpl, "marketplace/gateways.html", &ctx) } /// Renders the applications marketplace page pub async fn applications(tmpl: web::Data, session: Session, query: web::Query>) -> Result { let product_service = ProductService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let currency_service = CurrencyService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "applications"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Get user's preferred currency let display_currency = currency_service.get_user_preferred_currency(&session); // Parse pagination and filter parameters let page = query.get("page") .and_then(|p| p.parse::().ok()) .unwrap_or(0); let page_size = 12; // Products per page // Build search criteria from query parameters let mut criteria = crate::services::product::ProductSearchCriteria::new() .with_category("application".to_string()); // Add filters from query parameters if let Some(app_type) = query.get("app_type") { if !app_type.is_empty() { criteria = criteria.with_attribute("app_type".to_string(), serde_json::Value::String(app_type.clone())); } } if let Some(deployment_type) = query.get("deployment_type") { if !deployment_type.is_empty() { criteria = criteria.with_attribute("deployment_type".to_string(), serde_json::Value::String(deployment_type.clone())); } } let mut min_price_val = None; let mut max_price_val = None; if let Some(min_price) = query.get("min_price") { if let Ok(price) = min_price.parse::() { min_price_val = Some(price); } } if let Some(max_price) = query.get("max_price") { if let Ok(price) = max_price.parse::() { max_price_val = Some(price); } } if min_price_val.is_some() || max_price_val.is_some() { criteria = criteria.with_price_range(min_price_val, max_price_val); } // Search products with pagination let search_result = product_service.search_products_advanced(&criteria, page, page_size); // Debug: Log all product IDs found for product in &search_result.products { } // Get user-created apps from all users' persistent data (global marketplace) let mut all_applications = search_result.products.clone(); // Load apps from all users' persistent data for marketplace display let user_apps = crate::services::user_persistence::UserPersistence::get_all_users_apps(); // Convert user apps to marketplace products and add them // Collect products to register in MockDataService let mut products_to_register = Vec::new(); for app in user_apps { let marketplace_product = create_marketplace_product_from_app(&app); let availability = marketplace_product.availability.clone(); // Check if product already exists in all_applications and update or add accordingly if let Some(existing_index) = all_applications.iter().position(|p| p.id == marketplace_product.id) { // Update existing product with new availability status all_applications[existing_index] = marketplace_product.clone(); products_to_register.push((marketplace_product, app.name.clone())); } else { // Add new product all_applications.push(marketplace_product.clone()); products_to_register.push((marketplace_product, app.name.clone())); } } // Product registration is now handled through persistent user data // ProductService automatically aggregates products from user-owned data // Cart functionality works with persistent product catalog // This approach ensures proper separation: // - Dashboard: Shows only current user's apps from their persistent data // - Marketplace: Shows all apps from all users' persistent data (global aggregation) // Convert prices to user's currency let products_with_prices = match product_service.get_products_with_converted_prices( &all_applications, &display_currency, ) { Ok(converted) => converted.into_iter() .map(|(product, price)| { let formatted_price = currency_service.format_price( price.display_amount, &price.display_currency, ).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency)); serde_json::json!({ "product": product, "price": price, "formatted_price": formatted_price }) }) .collect::>(), Err(_) => Vec::new(), }; ctx.insert("application_products", &products_with_prices); ctx.insert("currencies", ¤cy_service.get_currency_display_info()); ctx.insert("user_currency", &display_currency); // Calculate updated pagination data including user apps let total_applications = all_applications.len(); let updated_total_pages = (total_applications + page_size - 1) / page_size; // Add pagination data ctx.insert("pagination", &serde_json::json!({ "current_page": search_result.page, "total_pages": updated_total_pages, "total_count": total_applications, "page_size": search_result.page_size, "has_previous": search_result.page > 0, "has_next": search_result.page < updated_total_pages.saturating_sub(1), "previous_page": search_result.page.saturating_sub(1), "next_page": search_result.page + 1 })); // Add current filters to context ctx.insert("current_filters", &query.into_inner()); // Add user data to context if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); if let Ok(user) = serde_json::from_str::(&user_json) { ctx.insert("user", &user); } } render_template(&tmpl, "marketplace/applications.html", &ctx) } /// Renders the services marketplace page pub async fn services(tmpl: web::Data, session: Session, query: web::Query>) -> Result { let product_service = ProductService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let currency_service = CurrencyService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "services"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Get user's preferred currency let display_currency = currency_service.get_user_preferred_currency(&session); // Parse pagination and filter parameters let page = query.get("page") .and_then(|p| p.parse::().ok()) .unwrap_or(0); let page_size = 12; // Products per page // Build search criteria from query parameters // Accept both "service" and "application" categories for backward compatibility let mut criteria = crate::services::product::ProductSearchCriteria::new(); // Note: Don't filter by category here - let all products through and filter in display // Add filters from query parameters if let Some(service_type) = query.get("service_type") { if !service_type.is_empty() { criteria = criteria.with_attribute("service_type".to_string(), serde_json::Value::String(service_type.clone())); } } if let Some(provider_rating) = query.get("provider_rating") { if !provider_rating.is_empty() { criteria = criteria.with_attribute("provider_rating".to_string(), serde_json::Value::String(provider_rating.clone())); } } let mut min_price_val = None; let mut max_price_val = None; if let Some(min_price) = query.get("min_price") { if let Ok(price) = min_price.parse::() { min_price_val = Some(price); } } if let Some(max_price) = query.get("max_price") { if let Ok(price) = max_price.parse::() { max_price_val = Some(price); } } if min_price_val.is_some() || max_price_val.is_some() { criteria = criteria.with_price_range(min_price_val, max_price_val); } // Search products with pagination let search_result = product_service.search_products_advanced(&criteria, page, page_size); // TEMP DEBUG: Check if we're actually getting all users' products let all_raw_products = product_service.get_all_products(); let service_products: Vec<_> = all_raw_products.iter() .filter(|p| p.category_id == "service") .collect(); println!("🔍 MARKETPLACE: Found {} total service products from all users", service_products.len()); // Debug: Log all product IDs found for product in &search_result.products { } // Filter for service-related products (both "service" and "application" categories) // This handles backward compatibility with existing products let all_services: Vec<_> = search_result.products.into_iter() .filter(|product| { let is_service_category = product.category_id == "service" || product.category_id == "application"; if is_service_category { println!("🔍 MARKETPLACE: Including product {} (category: {})", product.name, product.category_id); } is_service_category }) .collect(); println!("🔍 MARKETPLACE: Filtered to {} service/application products", all_services.len()); // Convert prices to user's currency let products_with_prices = match product_service.get_products_with_converted_prices( &all_services, &display_currency, ) { Ok(converted) => converted.into_iter() .map(|(product, price)| { let formatted_price = currency_service.format_price( price.display_amount, &price.display_currency, ).unwrap_or_else(|_| format!("{} {}", price.display_amount, price.display_currency)); serde_json::json!({ "product": product, "price": price, "formatted_price": formatted_price }) }) .collect::>(), Err(_) => Vec::new(), }; ctx.insert("service_products", &products_with_prices); ctx.insert("currencies", ¤cy_service.get_currency_display_info()); ctx.insert("user_currency", &display_currency); // Calculate updated pagination data including user services let total_services = all_services.len(); let updated_total_pages = (total_services + page_size - 1) / page_size; // Add pagination data ctx.insert("pagination", &serde_json::json!({ "current_page": search_result.page, "total_pages": updated_total_pages, "total_count": total_services, "page_size": search_result.page_size, "has_previous": search_result.page > 0, "has_next": search_result.page < updated_total_pages.saturating_sub(1), "previous_page": search_result.page.saturating_sub(1), "next_page": search_result.page + 1 })); // Add current filters to context ctx.insert("current_filters", &query.into_inner()); // Add user data to context if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); if let Ok(user) = serde_json::from_str::(&user_json) { ctx.insert("user", &user); } } render_template(&tmpl, "marketplace/services.html", &ctx) } /// Renders the marketplace statistics page pub async fn statistics(tmpl: web::Data, session: Session) -> Result { let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "statistics"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Add user data to context if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); if let Ok(user) = serde_json::from_str::(&user_json) { ctx.insert("user", &user); } } render_template(&tmpl, "marketplace/statistics.html", &ctx) } /// Show slice rental form with deployment options pub async fn show_slice_rental_form( tmpl: web::Data, session: Session, path: web::Path<(String, String, String)> // farmer_email, node_id, combination_id ) -> Result { let mut ctx = crate::models::builders::ContextBuilder::new() .active_page("marketplace") .build(); ctx.insert("active_section", "compute"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Add user data to context if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); if let Ok(user) = serde_json::from_str::(&user_json) { ctx.insert("user", &user); } } let (farmer_email, node_id, combination_id) = path.into_inner(); // Get slice details for the form let node_marketplace_service = NodeMarketplaceService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let slice_combinations = node_marketplace_service.get_all_slice_combinations(); if !slice_combinations.is_empty() { // Find the specific slice combination by checking product attributes if let Some(slice_product) = slice_combinations.iter().find(|p| { // Check if this product matches the requested slice if let (Some(farmer_attr), Some(node_attr), Some(combo_attr)) = ( p.attributes.get("farmer_email"), p.attributes.get("node_id"), p.attributes.get("combination_id") ) { farmer_attr.value.as_str() == Some(&farmer_email) && node_attr.value.as_str() == Some(&node_id) && combo_attr.value.as_str() == Some(&combination_id) } else { false } }) { // Extract slice information from product attributes let mut slice_info = std::collections::HashMap::new(); if let Some(specs) = slice_product.attributes.get("slice_specs") { if let Some(cpu) = specs.value.get("cpu_cores") { slice_info.insert("cpu_cores", cpu.clone()); } if let Some(memory) = specs.value.get("memory_gb") { slice_info.insert("memory_gb", memory.clone()); } if let Some(storage) = specs.value.get("storage_gb") { slice_info.insert("storage_gb", storage.clone()); } } if let Some(availability) = slice_product.attributes.get("availability") { if let Some(quantity) = availability.value.get("quantity_available") { slice_info.insert("available_quantity", quantity.clone()); } } slice_info.insert("price_per_hour", serde_json::Value::String(slice_product.base_price.to_string())); slice_info.insert("node_id", serde_json::Value::String(node_id.clone())); slice_info.insert("farmer_email", serde_json::Value::String(farmer_email.clone())); slice_info.insert("combination_id", serde_json::Value::String(combination_id.clone())); ctx.insert("slice", &slice_info); ctx.insert("farmer_email", &farmer_email); ctx.insert("node_id", &node_id); ctx.insert("combination_id", &combination_id); } } render_template(&tmpl, "marketplace/slice_rental_form.html", &ctx) } /// Handle slice rental form submission with enhanced deployment options pub async fn process_slice_rental( session: Session, form: web::Form ) -> Result { // Check if user is logged in let user_email = match session.get::("user_email")? { Some(email) => email, None => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "Please log in to rent slices" })).build(); } }; // Validate deployment type if !matches!(form.deployment_type.as_str(), "vm" | "kubernetes") { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "Invalid deployment type. Must be 'vm' or 'kubernetes'" })).build(); } // Validate deployment-specific fields let deployment_name = match form.deployment_type.as_str() { "vm" => { if form.vm_name.is_none() { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "VM name is required for VM deployments" })).build(); } form.vm_name.as_ref().unwrap().clone() }, "kubernetes" => { if form.cluster_name.is_none() || form.k8s_masters.is_none() || form.k8s_workers.is_none() { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "Cluster name, masters count, and workers count are required for Kubernetes deployments" })).build(); } form.cluster_name.as_ref().unwrap().clone() }, _ => unreachable!() }; // Create deployment configuration let deployment_config = SliceDeploymentConfig { deployment_id: uuid::Uuid::new_v4().to_string(), deployment_type: match form.deployment_type.as_str() { "vm" => DeploymentType::VM { name: form.vm_name.clone().unwrap_or_default(), os: form.vm_os.clone().unwrap_or_else(|| "ubuntu".to_string()), ssh_key: form.vm_ssh_key.clone(), }, "kubernetes" => DeploymentType::Kubernetes { cluster_name: form.cluster_name.clone().unwrap_or_default(), masters: form.k8s_masters.unwrap_or(1), workers: form.k8s_workers.unwrap_or(1), version: form.k8s_version.clone().unwrap_or_else(|| "1.29".to_string()), network_plugin: form.k8s_network_plugin.clone().unwrap_or_else(|| "flannel".to_string()), }, _ => unreachable!() }, slice_specs: SliceSpecs { cpu_cores: 0, // Will be filled by service memory_gb: 0, // Will be filled by service storage_gb: 0, // Will be filled by service quantity: form.quantity, }, network_config: NetworkConfig { public_ip: true, private_network: "10.0.0.0/24".to_string(), ports: vec![22, 80, 443], }, security_config: SecurityConfig { firewall_enabled: true, ssh_access: true, monitoring: form.monitoring_enabled.unwrap_or(false), backup: form.backup_enabled.unwrap_or(false), }, created_at: chrono::Utc::now(), status: DeploymentStatus::Pending, }; // Build slice rental service let slice_rental_service = SliceRentalService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Convert deployment config to JSON for the service let deployment_config_json = serde_json::to_value(&deployment_config) .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Attempt to rent the slice with deployment options match slice_rental_service.rent_slice_combination_with_deployment( &user_email, &form.farmer_email, &form.node_id, &form.combination_id, form.quantity, form.rental_duration_hours, &form.deployment_type, &deployment_name, Some(deployment_config_json), ) { Ok(rental) => { // Create user activity record let activity = crate::models::user::UserActivity { id: uuid::Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Rented {} slice(s) for {} deployment '{}'", form.quantity, form.deployment_type, deployment_name), timestamp: chrono::Utc::now(), metadata: Some({ let mut meta = std::collections::HashMap::new(); meta.insert("rental_id".to_string(), serde_json::Value::String(rental.rental_id.clone())); meta.insert("deployment_type".to_string(), serde_json::Value::String(form.deployment_type.clone())); meta.insert("deployment_name".to_string(), serde_json::Value::String(deployment_name.clone())); meta.insert("quantity".to_string(), serde_json::Value::Number(serde_json::Number::from(form.quantity))); meta.insert("farmer_email".to_string(), serde_json::Value::String(form.farmer_email.clone())); if form.deployment_type == "kubernetes" { meta.insert("k8s_masters".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_masters.unwrap_or(1)))); meta.insert("k8s_workers".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_workers.unwrap_or(1)))); } serde_json::Value::Object(meta.into_iter().collect()) }), category: "slice_rental".to_string(), importance: crate::models::user::ActivityImportance::High, ip_address: None, user_agent: None, session_id: None, }; // Add activity to user's record if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let _ = user_service.add_user_activity(&user_email, activity); } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "message": format!("Slice rental successful! {} deployment '{}' is being set up.", form.deployment_type.to_uppercase(), deployment_name), "rental_id": rental.rental_id, "total_cost": rental.total_cost, "deployment_type": form.deployment_type, "deployment_name": deployment_name, "redirect_url": "/dashboard/user" })).build() }, Err(error) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": error })).build() } } } /// Handle slice rental requests with deployment options (legacy method) pub async fn rent_slice( session: Session, form: web::Form ) -> Result { // Check if user is logged in let user_email = match session.get::("user_email")? { Some(email) => email, None => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "Please log in to rent slices" })).build(); } }; // Validate deployment type if !matches!(form.deployment_type.as_str(), "vm" | "kubernetes") { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "Invalid deployment type. Must be 'vm' or 'kubernetes'" })).build(); } // Build slice rental service let slice_rental_service = SliceRentalService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Attempt to rent the slice with deployment options match slice_rental_service.rent_slice_combination_with_deployment( &user_email, &form.farmer_email, &form.node_id, &form.combination_id, form.quantity, form.rental_duration_hours, &form.deployment_type, &form.deployment_name, form.deployment_config.clone(), ) { Ok(rental) => { // Create user activity record let activity = crate::models::user::UserActivity { id: uuid::Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Rented {} slice(s) for {} deployment '{}'", form.quantity, form.deployment_type, form.deployment_name), timestamp: chrono::Utc::now(), metadata: Some({ let mut meta = std::collections::HashMap::new(); meta.insert("rental_id".to_string(), serde_json::Value::String(rental.rental_id.clone())); meta.insert("deployment_type".to_string(), serde_json::Value::String(form.deployment_type.clone())); meta.insert("deployment_name".to_string(), serde_json::Value::String(form.deployment_name.clone())); meta.insert("quantity".to_string(), serde_json::Value::Number(serde_json::Number::from(form.quantity))); meta.insert("farmer_email".to_string(), serde_json::Value::String(form.farmer_email.clone())); serde_json::Value::Object(meta.into_iter().collect()) }), category: "slice_rental".to_string(), importance: crate::models::user::ActivityImportance::High, ip_address: None, user_agent: None, session_id: None, }; // Add activity to user's record if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let _ = user_service.add_user_activity(&user_email, activity); } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "message": format!("Slice rental successful! {} deployment '{}' is being set up.", form.deployment_type.to_uppercase(), form.deployment_name), "rental_id": rental.rental_id, "total_cost": rental.total_cost, "deployment_type": form.deployment_type, "deployment_name": form.deployment_name })).build() }, Err(error) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": error })).build() } } } } /// Request structure for slice rental with deployment options #[derive(serde::Deserialize)] pub struct SliceRentalRequest { pub farmer_email: String, pub node_id: String, pub combination_id: String, pub quantity: u32, pub rental_duration_hours: u32, pub deployment_type: String, // "vm" or "kubernetes" pub deployment_name: String, pub deployment_config: Option, // Additional config for K8s clusters } /// Helper function to convert a Service to a marketplace Product pub fn create_marketplace_product_from_service(service: &crate::models::user::Service) -> crate::models::product::Product { use crate::models::product::{Product, ProductAttribute, ProductAvailability, ProductMetadata, AttributeType}; let availability = if service.status == "Active" { ProductAvailability::Available } else { ProductAvailability::Unavailable }; let metadata = ProductMetadata { tags: vec![service.category.clone(), "user-created".to_string()], location: Some("Remote".to_string()), rating: Some(service.rating), review_count: 0, featured: false, last_updated: chrono::Utc::now(), visibility: crate::models::product::ProductVisibility::Public, seo_keywords: Vec::new(), custom_fields: std::collections::HashMap::new(), }; Product::builder() .id(&service.id) .name(&service.name) .description(&service.description) .category_id("service") .base_price(rust_decimal::Decimal::from(service.price_per_hour_usd)) .base_currency("USD") .provider_id("user-service-provider") .provider_name("Service Provider") .availability(availability) .metadata(metadata) .add_attribute("service_type", ProductAttribute { key: "service_type".to_string(), value: serde_json::Value::String(service.category.clone()), attribute_type: AttributeType::Text, is_searchable: true, is_filterable: true, display_order: Some(1), }) .add_attribute("provider_rating", ProductAttribute { key: "provider_rating".to_string(), value: serde_json::Value::Number(serde_json::Number::from_f64(service.rating as f64).unwrap_or(serde_json::Number::from(0))), attribute_type: AttributeType::Number, is_searchable: false, is_filterable: true, display_order: Some(2), }) .add_attribute("experience_level", ProductAttribute { key: "experience_level".to_string(), value: serde_json::Value::String("intermediate".to_string()), attribute_type: AttributeType::Text, is_searchable: true, is_filterable: true, display_order: Some(3), }) .add_attribute("response_time", ProductAttribute { key: "response_time".to_string(), value: serde_json::Value::String("24 hours".to_string()), attribute_type: AttributeType::Text, is_searchable: true, is_filterable: false, display_order: Some(4), }) .build() .unwrap() } /// Helper function to convert a PublishedApp to a marketplace Product pub fn create_marketplace_product_from_app(app: &crate::models::user::PublishedApp) -> crate::models::product::Product { use crate::models::product::{Product, ProductAttribute, ProductAvailability, ProductMetadata, AttributeType}; let availability = if app.status == "Active" { ProductAvailability::Available } else { ProductAvailability::Unavailable }; let metadata = ProductMetadata { tags: vec![app.category.clone(), "user-created".to_string()], location: Some("Cloud".to_string()), rating: Some(app.rating), review_count: 0, featured: false, last_updated: chrono::Utc::now(), visibility: crate::models::product::ProductVisibility::Public, seo_keywords: Vec::new(), custom_fields: std::collections::HashMap::new(), }; Product::builder() .id(&app.id) .name(&app.name) .description(format!("Application: {} (Version {})", app.name, app.version)) .category_id("application") .base_price(rust_decimal::Decimal::from(app.monthly_revenue_usd.max(rust_decimal::Decimal::ONE))) // Use monthly revenue as base price, minimum $1 .base_currency("USD") .provider_id("user-app-provider") .provider_name("App Provider") .availability(availability) .metadata(metadata) .add_attribute("app_type", ProductAttribute { key: "app_type".to_string(), value: serde_json::Value::String(app.category.clone()), attribute_type: AttributeType::Text, is_searchable: true, is_filterable: true, display_order: Some(1), }) .add_attribute("deployment_type", ProductAttribute { key: "deployment_type".to_string(), value: serde_json::Value::String("Container".to_string()), // Default deployment type attribute_type: AttributeType::Text, is_searchable: true, is_filterable: true, display_order: Some(2), }) .add_attribute("version", ProductAttribute { key: "version".to_string(), value: serde_json::Value::String(app.version.clone()), attribute_type: AttributeType::Text, is_searchable: true, is_filterable: false, display_order: Some(3), }) .add_attribute("deployments", ProductAttribute { key: "deployments".to_string(), value: serde_json::Value::Number(serde_json::Number::from(app.deployments)), attribute_type: AttributeType::Number, is_searchable: false, is_filterable: false, display_order: Some(4), }) .build() .unwrap() } /// Helper function to reconstruct a Service from JSON data (fallback mechanism) fn reconstruct_service_from_json(service_value: &serde_json::Value) -> Result> { let price_per_hour = service_value.get("price_per_hour") .and_then(|v| v.as_i64()) .unwrap_or(0) as i32; let service = crate::models::user::Service { id: service_value.get("id") .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_string(), name: service_value.get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown Service") .to_string(), category: service_value.get("category") .and_then(|v| v.as_str()) .unwrap_or("General") .to_string(), description: service_value.get("description") .and_then(|v| v.as_str()) .unwrap_or("No description available") .to_string(), price_usd: rust_decimal::Decimal::new(price_per_hour as i64, 0), hourly_rate_usd: Some(rust_decimal::Decimal::new(price_per_hour as i64, 0)), availability: true, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), price_per_hour_usd: price_per_hour, status: service_value.get("status") .and_then(|v| v.as_str()) .unwrap_or("Active") .to_string(), clients: service_value.get("clients") .and_then(|v| v.as_i64()) .unwrap_or(0) as i32, rating: service_value.get("rating") .and_then(|v| v.as_f64()) .unwrap_or(0.0) as f32, total_hours: Some(service_value.get("total_hours") .and_then(|v| v.as_i64()) .unwrap_or(0) as i32), }; Ok(service) } /// Handle slice rental requests from marketplace pub async fn rent_slice( form: web::Form, session: Session, ) -> Result { // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice rental service let slice_rental_service = match SliceRentalService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Validate deployment configuration if form.deployment_type == "kubernetes" { if form.k8s_masters.is_none() || form.k8s_workers.is_none() || form.cluster_name.is_none() { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "Kubernetes deployment requires masters, workers, and cluster name" })).build(); } } // Attempt to rent the slice combination match slice_rental_service.rent_slice_combination( &user_email, &form.farmer_email, &form.node_id, &form.combination_id, form.quantity, form.rental_duration_hours, ) { Ok(rental) => { // Create deployment metadata based on type let mut deployment_metadata = std::collections::HashMap::new(); deployment_metadata.insert("deployment_type".to_string(), serde_json::Value::String(form.deployment_type.clone())); deployment_metadata.insert("rental_duration_hours".to_string(), serde_json::Value::Number(serde_json::Number::from(form.rental_duration_hours))); if form.deployment_type == "kubernetes" { deployment_metadata.insert("k8s_masters".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_masters.unwrap_or(1)))); deployment_metadata.insert("k8s_workers".to_string(), serde_json::Value::Number(serde_json::Number::from(form.k8s_workers.unwrap_or(2)))); deployment_metadata.insert("cluster_name".to_string(), serde_json::Value::String(form.cluster_name.clone().unwrap_or_else(|| format!("cluster-{}", Uuid::new_v4().to_string()[..8].to_string())))); } // Add user activity for slice rental if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let activity = crate::models::user::UserActivity { id: Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Rented {} slice(s) for {} deployment", form.quantity, form.deployment_type), timestamp: Utc::now(), metadata: Some(serde_json::Value::Object(deployment_metadata.clone().into_iter().collect())), category: "Slice Rental".to_string(), importance: crate::models::user::ActivityImportance::High, ip_address: None, user_agent: None, session_id: None, }; if let Err(e) = user_service.add_user_activity(&user_email, activity) { } /// Create a new slice assignment for deployment pub async fn create_slice_assignment( session: Session, assignment_data: web::Json, ) -> Result { // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice assignment service let slice_assignment_service = match SliceAssignmentService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Extract assignment details from request let assignment_request = match assignment_data.into_inner() { serde_json::Value::Object(map) => map, _ => { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "Invalid assignment data format" })).build(); } }; // Validate required fields let slice_id = match assignment_request.get("slice_id").and_then(|v| v.as_str()) { Some(id) => id.to_string(), None => { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "slice_id is required" })).build(); } }; let deployment_type = match assignment_request.get("deployment_type").and_then(|v| v.as_str()) { Some(dt) => dt.to_string(), None => { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "deployment_type is required" })).build(); } }; // Create SliceAssignmentRequest from the data let assignment_request_obj = SliceAssignmentRequest { user_email: user_email.clone(), farmer_email: assignment_request.get("farmer_email") .and_then(|v| v.as_str()) .unwrap_or("unknown@example.com") .to_string(), node_id: assignment_request.get("node_id") .and_then(|v| v.as_str()) .unwrap_or(&slice_id) .to_string(), combination_id: slice_id.clone(), quantity: assignment_request.get("quantity") .and_then(|v| v.as_u64()) .unwrap_or(1) as u32, rental_duration_hours: assignment_request.get("rental_duration_hours") .and_then(|v| v.as_u64()) .unwrap_or(24) as u32, total_cost: rust_decimal::Decimal::from( assignment_request.get("total_cost") .and_then(|v| v.as_i64()) .unwrap_or(50) as i32 ), deployment_config: serde_json::from_value(serde_json::Value::Object(assignment_request.clone())) .unwrap_or_else(|_| DeploymentConfiguration { deployment_type: SliceDeploymentType::IndividualVM { vm_configs: vec![VMConfiguration { vm_name: "default-vm".to_string(), os_image: "ubuntu-22.04".to_string(), ssh_key: None, slice_count: 1, auto_scaling: false, custom_startup_script: None, }], }, assignment_mode: AssignmentMode::IndividualVMs, network_config: NetworkConfiguration { public_ip_required: false, private_network_cidr: Some("10.0.0.0/24".to_string()), exposed_ports: vec![], load_balancer_enabled: false, }, security_config: SecurityConfiguration { firewall_enabled: true, ssh_access_enabled: true, vpn_access_enabled: false, encryption_at_rest: true, encryption_in_transit: true, }, monitoring_enabled: true, backup_enabled: false, }), }; // Create the assignment match slice_assignment_service.create_assignment(assignment_request_obj) { Ok(assignment) => { // Add user activity if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let activity = crate::models::user::UserActivity { id: Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Created {} deployment assignment for slice {}", deployment_type, slice_id), timestamp: Utc::now(), metadata: Some(serde_json::Value::Object(assignment_request.into_iter().collect())), category: "Slice Assignment".to_string(), importance: crate::models::user::ActivityImportance::High, ip_address: None, user_agent: None, session_id: None, }; let _ = user_service.add_user_activity(&user_email, activity); } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "assignment": assignment, "message": "Slice assignment created successfully" })).build() } Err(e) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": format!("Assignment creation failed: {}", e) })).build() } } } /// Get all slice assignments for the authenticated user pub async fn get_slice_assignments(session: Session) -> Result { // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice assignment service let slice_assignment_service = match SliceAssignmentService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Get user's assignments match slice_assignment_service.get_user_assignments(&user_email) { Ok(assignments) => { ResponseBuilder::ok().json(serde_json::json!({ "success": true, "assignments": assignments, "count": assignments.len() })).build() } Err(e) => { ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": format!("Failed to retrieve assignments: {}", e) })).build() } } } /// Get details for a specific slice assignment pub async fn get_slice_assignment_details( session: Session, path: web::Path, ) -> Result { let assignment_id = path.into_inner(); // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice assignment service let slice_assignment_service = match SliceAssignmentService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Get assignment details match slice_assignment_service.get_assignment_details(&assignment_id, &user_email) { Ok(Some(assignment)) => { ResponseBuilder::ok().json(serde_json::json!({ "success": true, "assignment": assignment })).build() } Ok(None) => { ResponseBuilder::not_found().json(serde_json::json!({ "success": false, "message": "Assignment not found or access denied" })).build() } Err(e) => { ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": format!("Failed to retrieve assignment details: {}", e) })).build() } } } /// Update a slice assignment configuration pub async fn update_slice_assignment( session: Session, path: web::Path, update_data: web::Json, ) -> Result { let assignment_id = path.into_inner(); // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice assignment service let slice_assignment_service = match SliceAssignmentService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Extract update data let update_config = match update_data.into_inner() { serde_json::Value::Object(map) => map, _ => { return ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": "Invalid update data format" })).build(); } }; // Update the assignment match slice_assignment_service.update_assignment(&assignment_id, &user_email, update_config.clone().into_iter().collect()) { Ok(updated_assignment) => { // Add user activity if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let activity = crate::models::user::UserActivity { id: Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Updated slice assignment configuration for {}", assignment_id), timestamp: Utc::now(), metadata: Some(serde_json::Value::Object(update_config.into_iter().collect())), category: "Slice Assignment".to_string(), importance: crate::models::user::ActivityImportance::Medium, ip_address: None, user_agent: None, session_id: None, }; let _ = user_service.add_user_activity(&user_email, activity); } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "assignment": updated_assignment, "message": "Assignment updated successfully" })).build() } Err(e) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": format!("Assignment update failed: {}", e) })).build() } } } /// Delete a slice assignment pub async fn delete_slice_assignment( session: Session, path: web::Path, ) -> Result { let assignment_id = path.into_inner(); // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice assignment service let slice_assignment_service = match SliceAssignmentService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Delete the assignment match slice_assignment_service.delete_assignment(&assignment_id, &user_email) { Ok(()) => { // Add user activity if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let activity = crate::models::user::UserActivity { id: Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Deleted slice assignment {}", assignment_id), timestamp: Utc::now(), metadata: Some(serde_json::Value::Object(std::collections::HashMap::new().into_iter().collect())), category: "Slice Assignment".to_string(), importance: crate::models::user::ActivityImportance::Medium, ip_address: None, user_agent: None, session_id: None, }; let _ = user_service.add_user_activity(&user_email, activity); } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "message": "Assignment deleted successfully" })).build() } Err(e) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": format!("Assignment deletion failed: {}", e) })).build() } } } /// Deploy a slice assignment (start the actual deployment) pub async fn deploy_slice_assignment( session: Session, path: web::Path, ) -> Result { let assignment_id = path.into_inner(); // Get user email from session let user_email = match session.get::("user_email") { Ok(Some(email)) => email, _ => { return ResponseBuilder::unauthorized().json(serde_json::json!({ "success": false, "message": "User not authenticated" })).build(); } }; // Build slice assignment service let slice_assignment_service = match SliceAssignmentService::builder().build() { Ok(service) => service, Err(e) => { return ResponseBuilder::internal_error().json(serde_json::json!({ "success": false, "message": "Service initialization failed" })).build(); } }; // Deploy the assignment match slice_assignment_service.deploy_assignment(&assignment_id, &user_email) { Ok(deployment_info) => { // Add user activity if let Ok(user_service) = crate::services::user_service::UserService::builder().build() { let activity = crate::models::user::UserActivity { id: Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: crate::models::user::ActivityType::SliceAllocated, description: format!("Deployed slice assignment {}", assignment_id), timestamp: Utc::now(), metadata: Some(serde_json::Value::Object(std::collections::HashMap::from([ ("assignment_id".to_string(), serde_json::Value::String(assignment_id.clone())), ("deployment_info".to_string(), serde_json::to_value(&deployment_info).unwrap_or_default()), ]).into_iter().collect())), category: "Slice Deployment".to_string(), importance: crate::models::user::ActivityImportance::High, ip_address: None, user_agent: None, session_id: None, }; let _ = user_service.add_user_activity(&user_email, activity); } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "deployment": deployment_info, "message": "Assignment deployed successfully" })).build() } Err(e) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": format!("Deployment failed: {}", e) })).build() } } } } ResponseBuilder::ok().json(serde_json::json!({ "success": true, "message": "Slice rental successful", "rental_id": rental.rental_id, "deployment_type": form.deployment_type, "rental_duration_hours": form.rental_duration_hours, "metadata": deployment_metadata })).build() } Err(e) => { ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": format!("Rental failed: {}", e) })).build() } } }