437 lines
19 KiB
Rust
437 lines
19 KiB
Rust
use crate::models::user::{UserActivity, UsageStatistics, UserPreferences, Transaction};
|
|
use crate::services::user_persistence::UserPersistence;
|
|
use rust_decimal::Decimal;
|
|
use chrono::{Utc, Datelike};
|
|
use std::collections::HashMap;
|
|
|
|
/// Configuration for UserService
|
|
#[derive(Debug, Clone)]
|
|
pub struct UserServiceConfig {
|
|
pub activity_limit: usize,
|
|
}
|
|
|
|
impl Default for UserServiceConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
activity_limit: 50,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Builder for UserService following established pattern
|
|
#[derive(Default)]
|
|
pub struct UserServiceBuilder {
|
|
activity_limit: Option<usize>,
|
|
}
|
|
|
|
impl UserServiceBuilder {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn activity_limit(mut self, limit: usize) -> Self {
|
|
self.activity_limit = Some(limit);
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Result<UserService, String> {
|
|
let config = UserServiceConfig {
|
|
activity_limit: self.activity_limit.unwrap_or(50),
|
|
};
|
|
|
|
Ok(UserService { config })
|
|
}
|
|
}
|
|
|
|
/// Main UserService for managing user dashboard data
|
|
#[derive(Clone)]
|
|
pub struct UserService {
|
|
config: UserServiceConfig,
|
|
}
|
|
|
|
impl UserService {
|
|
/// Create a new builder for UserService
|
|
pub fn builder() -> UserServiceBuilder {
|
|
UserServiceBuilder::new()
|
|
}
|
|
|
|
/// Get user activities with optional limit
|
|
pub fn get_user_activities(&self, user_email: &str, limit: Option<usize>) -> Vec<UserActivity> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
let mut activities = persistent_data.user_activities;
|
|
|
|
// Sort by timestamp (newest first)
|
|
activities.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
|
|
|
// Apply limit
|
|
let limit = limit.unwrap_or(self.config.activity_limit);
|
|
activities.truncate(limit);
|
|
activities
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get user purchase history from transactions
|
|
pub fn get_purchase_history(&self, user_email: &str) -> Vec<Transaction> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
let purchases: Vec<Transaction> = persistent_data.transactions
|
|
.into_iter()
|
|
.filter(|t| matches!(t.transaction_type, crate::models::user::TransactionType::Purchase { .. }))
|
|
.collect();
|
|
purchases
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get or calculate usage statistics
|
|
pub fn get_usage_statistics(&self, user_email: &str) -> Option<UsageStatistics> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
// Return existing statistics or calculate new ones
|
|
if let Some(stats) = persistent_data.usage_statistics {
|
|
Some(stats)
|
|
} else {
|
|
Some(self.calculate_usage_statistics(user_email, &persistent_data))
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Get active deployments for user
|
|
pub fn get_active_deployments(&self, user_email: &str) -> Vec<crate::models::user::UserDeployment> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
// Convert active product rentals to deployments
|
|
let deployments: Vec<crate::models::user::UserDeployment> = persistent_data.active_product_rentals
|
|
.into_iter()
|
|
.filter(|r| r.status == "Active")
|
|
.map(|rental| crate::models::user::UserDeployment {
|
|
id: rental.id,
|
|
app_name: rental.product_name,
|
|
status: crate::models::user::DeploymentStatus::Active,
|
|
cost_per_month: rental.monthly_cost,
|
|
deployed_at: chrono::DateTime::parse_from_rfc3339(&rental.start_date)
|
|
.map(|dt| dt.with_timezone(&Utc))
|
|
.unwrap_or_else(|_| Utc::now()),
|
|
provider: rental.provider_email,
|
|
region: rental.metadata.get("region")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Unknown")
|
|
.to_string(),
|
|
resource_usage: rental.metadata.get("resource_usage")
|
|
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
|
.unwrap_or_default(),
|
|
})
|
|
.collect();
|
|
deployments
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
|
|
/// Get user's published services (for service-provider dashboard)
|
|
pub fn get_user_published_services(&self, user_email: &str) -> Vec<crate::models::user::Service> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
persistent_data.services
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get user's purchased services (for user dashboard) - derived from service bookings
|
|
pub fn get_user_purchased_services(&self, user_email: &str) -> Vec<crate::models::user::Service> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
// Convert service bookings to service view for user dashboard
|
|
// IMPORTANT: Only show bookings where THIS user is the customer
|
|
let purchased_services: Vec<crate::models::user::Service> = persistent_data.service_bookings
|
|
.into_iter()
|
|
.filter(|b| b.customer_email == user_email)
|
|
.map(|booking| {
|
|
let hourly_rate = (booking.budget / rust_decimal::Decimal::from(booking.estimated_hours.unwrap_or(1).max(1))).to_string().parse::<i32>().unwrap_or(0);
|
|
crate::models::user::Service {
|
|
id: booking.service_id,
|
|
name: booking.service_name,
|
|
category: "Service".to_string(), // Default category for purchased services
|
|
description: booking.description.unwrap_or_else(|| "Service booking".to_string()),
|
|
price_usd: booking.budget,
|
|
hourly_rate_usd: Some(rust_decimal::Decimal::new(hourly_rate as i64, 0)),
|
|
availability: true,
|
|
created_at: chrono::DateTime::parse_from_rfc3339(&booking.requested_date)
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.unwrap_or_else(|_| chrono::Utc::now()),
|
|
updated_at: chrono::DateTime::parse_from_rfc3339(&booking.requested_date)
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.unwrap_or_else(|_| chrono::Utc::now()),
|
|
price_per_hour_usd: hourly_rate,
|
|
status: booking.status,
|
|
clients: 0, // Users don't see client count for purchased services
|
|
rating: 4.5, // Default rating
|
|
total_hours: booking.estimated_hours,
|
|
}
|
|
})
|
|
.collect();
|
|
purchased_services
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get user's purchased applications (for user dashboard) - derived from deployments
|
|
pub fn get_user_applications(&self, user_email: &str) -> Vec<crate::models::user::PublishedApp> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
// Convert application deployments to application view for user dashboard
|
|
// IMPORTANT: Only show deployments where THIS user is the customer
|
|
let purchased_apps: Vec<crate::models::user::PublishedApp> = persistent_data.application_deployments
|
|
.into_iter()
|
|
.filter(|d| d.status == "Active" && d.customer_email == user_email)
|
|
.map(|deployment| crate::models::user::PublishedApp {
|
|
id: deployment.app_id,
|
|
name: deployment.app_name,
|
|
description: Some("User-deployed application".to_string()),
|
|
category: "Application".to_string(), // Default category for purchased apps
|
|
version: "1.0.0".to_string(), // Default version
|
|
price_usd: rust_decimal::Decimal::ZERO,
|
|
deployment_count: 1,
|
|
status: deployment.status,
|
|
created_at: chrono::DateTime::parse_from_rfc3339(&deployment.created_at)
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.unwrap_or_else(|_| chrono::Utc::now()),
|
|
updated_at: chrono::DateTime::parse_from_rfc3339(&deployment.last_updated)
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.unwrap_or_else(|_| chrono::Utc::now()),
|
|
auto_scaling: Some(false),
|
|
auto_healing: deployment.auto_healing,
|
|
revenue_history: Vec::new(),
|
|
deployments: 1, // User has 1 deployment of this app
|
|
rating: 4.5, // Default rating
|
|
monthly_revenue_usd: rust_decimal::Decimal::ZERO, // Users don't earn revenue from purchased apps
|
|
last_updated: chrono::DateTime::parse_from_rfc3339(&deployment.last_updated)
|
|
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
.unwrap_or_else(|_| chrono::Utc::now()),
|
|
})
|
|
.collect();
|
|
purchased_apps
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Get user's active compute resources from rentals
|
|
pub fn get_user_compute_resources(&self, user_email: &str) -> Vec<crate::models::user::UserComputeResource> {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
let compute_resources: Vec<crate::models::user::UserComputeResource> = persistent_data.active_product_rentals
|
|
.into_iter()
|
|
.filter(|r| r.status == "Active")
|
|
.map(|rental| crate::models::user::UserComputeResource {
|
|
id: rental.product_name.clone(),
|
|
resource_type: rental.metadata.get("type")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Unknown")
|
|
.to_string(),
|
|
specs: rental.metadata.get("specs")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Unknown")
|
|
.to_string(),
|
|
location: rental.metadata.get("location")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Unknown")
|
|
.to_string(),
|
|
status: rental.status.clone(),
|
|
sla: rental.metadata.get("sla")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Unknown")
|
|
.to_string(),
|
|
monthly_cost: rental.monthly_cost,
|
|
provider: rental.provider_email,
|
|
resource_usage: rental.metadata.get("resource_usage")
|
|
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
|
.unwrap_or_default(),
|
|
})
|
|
.collect();
|
|
compute_resources
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Calculate comprehensive user metrics
|
|
pub fn calculate_user_metrics(&self, user_email: &str) -> crate::models::user::UserMetrics {
|
|
if let Some(persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
// Calculate total spent this month
|
|
let current_month = Utc::now().format("%Y-%m").to_string();
|
|
let total_spent_this_month: Decimal = persistent_data.transactions
|
|
.iter()
|
|
.filter(|t| {
|
|
t.timestamp.format("%Y-%m").to_string() == current_month &&
|
|
matches!(t.transaction_type, crate::models::user::TransactionType::Purchase { .. })
|
|
})
|
|
.map(|t| t.amount)
|
|
.sum();
|
|
|
|
// Count active deployments
|
|
let active_deployments_count = persistent_data.active_product_rentals
|
|
.iter()
|
|
.filter(|r| r.status == "Active")
|
|
.count() as i32;
|
|
|
|
// Calculate resource utilization from active rentals
|
|
let resource_utilization = self.calculate_resource_utilization(&persistent_data.active_product_rentals);
|
|
|
|
// Generate cost trend (last 6 months)
|
|
let cost_trend = self.calculate_cost_trend(&persistent_data.transactions);
|
|
|
|
crate::models::user::UserMetrics {
|
|
total_spent_this_month,
|
|
active_deployments_count,
|
|
resource_utilization,
|
|
cost_trend,
|
|
wallet_balance: persistent_data.wallet_balance_usd,
|
|
total_transactions: persistent_data.transactions.len() as i32,
|
|
}
|
|
} else {
|
|
crate::models::user::UserMetrics::default()
|
|
}
|
|
}
|
|
|
|
/// Add new user activity
|
|
pub fn add_user_activity(&self, user_email: &str, activity: UserActivity) -> Result<(), String> {
|
|
if let Some(mut persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
persistent_data.user_activities.push(activity);
|
|
|
|
// Keep only recent activities (limit to prevent file bloat)
|
|
persistent_data.user_activities.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
|
persistent_data.user_activities.truncate(100);
|
|
|
|
UserPersistence::save_user_data(&persistent_data)
|
|
.map_err(|e| format!("Failed to save user activity: {}", e))
|
|
} else {
|
|
Err("User data not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Update user preferences
|
|
pub fn update_user_preferences(&self, user_email: &str, preferences: UserPreferences) -> Result<(), String> {
|
|
if let Some(mut persistent_data) = UserPersistence::load_user_data(user_email) {
|
|
persistent_data.user_preferences = Some(preferences);
|
|
|
|
UserPersistence::save_user_data(&persistent_data)
|
|
.map_err(|e| format!("Failed to save user preferences: {}", e))
|
|
} else {
|
|
Err("User data not found".to_string())
|
|
}
|
|
}
|
|
|
|
// Private helper methods
|
|
fn calculate_usage_statistics(&self, _user_email: &str, persistent_data: &crate::services::user_persistence::UserPersistentData) -> UsageStatistics {
|
|
let total_deployments = persistent_data.active_product_rentals.len() as i32;
|
|
let total_spent = persistent_data.transactions
|
|
.iter()
|
|
.filter(|t| matches!(t.transaction_type, crate::models::user::TransactionType::Purchase { .. }))
|
|
.map(|t| t.amount)
|
|
.sum();
|
|
|
|
// Calculate favorite categories from purchase history
|
|
let mut category_counts: HashMap<String, i32> = HashMap::new();
|
|
for rental in &persistent_data.active_product_rentals {
|
|
if let Some(category) = rental.metadata.get("category").and_then(|v| v.as_str()) {
|
|
*category_counts.entry(category.to_string()).or_insert(0) += 1;
|
|
}
|
|
}
|
|
|
|
let mut favorite_categories_vec: Vec<(String, i32)> = category_counts
|
|
.into_iter()
|
|
.collect();
|
|
favorite_categories_vec.sort_by(|a, b| b.1.cmp(&a.1));
|
|
let favorite_categories = favorite_categories_vec
|
|
.into_iter()
|
|
.take(5)
|
|
.map(|(category, _)| category)
|
|
.collect();
|
|
|
|
UsageStatistics {
|
|
cpu_usage: 15.0, // TODO: Calculate from actual deployments
|
|
memory_usage: 45.0, // TODO: Calculate from actual deployments
|
|
storage_usage: 60.0, // TODO: Calculate from actual deployments
|
|
network_usage: 25.0, // TODO: Calculate from actual deployments
|
|
total_deployments,
|
|
active_services: persistent_data.services.len() as i32,
|
|
total_spent,
|
|
favorite_categories,
|
|
usage_trends: Vec::new(), // TODO: Implement trend calculation
|
|
login_frequency: 3.5, // TODO: Calculate from activity log
|
|
preferred_regions: vec!["Amsterdam".to_string(), "New York".to_string()], // TODO: Calculate from deployments
|
|
account_age_days: 90, // TODO: Calculate from creation date
|
|
last_activity: chrono::Utc::now(),
|
|
}
|
|
}
|
|
|
|
fn calculate_resource_utilization(&self, rentals: &[crate::services::user_persistence::ProductRental]) -> crate::models::user::ResourceUtilization {
|
|
// Calculate average resource utilization across active rentals
|
|
if rentals.is_empty() {
|
|
return crate::models::user::ResourceUtilization {
|
|
cpu: 0,
|
|
memory: 0,
|
|
storage: 0,
|
|
network: 0,
|
|
};
|
|
}
|
|
|
|
let mut total_cpu = 0;
|
|
let mut total_memory = 0;
|
|
let mut total_storage = 0;
|
|
let mut total_network = 0;
|
|
let mut count = 0;
|
|
|
|
for rental in rentals {
|
|
if let Some(usage) = rental.metadata.get("resource_usage") {
|
|
if let Ok(usage) = serde_json::from_value::<crate::models::user::ResourceUtilization>(usage.clone()) {
|
|
total_cpu += usage.cpu;
|
|
total_memory += usage.memory;
|
|
total_storage += usage.storage;
|
|
total_network += usage.network;
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if count > 0 {
|
|
crate::models::user::ResourceUtilization {
|
|
cpu: total_cpu / count,
|
|
memory: total_memory / count,
|
|
storage: total_storage / count,
|
|
network: total_network / count,
|
|
}
|
|
} else {
|
|
crate::models::user::ResourceUtilization {
|
|
cpu: 45, // Default reasonable values
|
|
memory: 60,
|
|
storage: 35,
|
|
network: 25,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn calculate_cost_trend(&self, transactions: &[Transaction]) -> Vec<i32> {
|
|
// Calculate monthly spending for last 6 months
|
|
let mut monthly_costs = vec![0; 6];
|
|
let current_date = Utc::now();
|
|
|
|
for transaction in transactions {
|
|
if matches!(transaction.transaction_type, crate::models::user::TransactionType::Purchase { .. }) {
|
|
let months_ago = (current_date.year() * 12 + current_date.month() as i32) -
|
|
(transaction.timestamp.year() * 12 + transaction.timestamp.month() as i32);
|
|
|
|
if months_ago >= 0 && months_ago < 6 {
|
|
let index = (5 - months_ago) as usize;
|
|
if index < monthly_costs.len() {
|
|
monthly_costs[index] += transaction.amount.to_string().parse::<i32>().unwrap_or(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
monthly_costs
|
|
}
|
|
} |