//! Cost Estimator //! //! Estimate costs for projected resource usage. use crate::error::Result; use crate::pricing::PricingEngine; use crate::{ResourceUnit, ServiceType, SynorDecimal}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; /// Usage projection for cost estimation #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct UsageProjection { /// Projected storage in GB pub storage_gb: Decimal, /// Projected storage retrieval in GB pub storage_retrieval_gb: Decimal, /// Projected hosting bandwidth in GB pub hosting_bandwidth_gb: Decimal, /// Projected custom domains pub custom_domains: u32, /// Projected database storage in GB pub database_storage_gb: Decimal, /// Projected database queries (in millions) pub database_queries_millions: Decimal, /// Projected vector searches (in millions) pub vector_searches_millions: Decimal, /// Projected CPU core-hours pub cpu_core_hours: Decimal, /// Projected GPU hours pub gpu_hours: Decimal, /// Projected memory GB-hours pub memory_gb_hours: Decimal, /// Projected serverless invocations (in millions) pub invocations_millions: Decimal, /// Projected network bandwidth in GB pub network_bandwidth_gb: Decimal, /// Duration in months (for monthly costs) pub duration_months: u32, /// Pricing tier to apply pub tier: Option, } impl UsageProjection { /// Create a new empty projection pub fn new() -> Self { Self { duration_months: 1, ..Default::default() } } /// Set storage usage pub fn with_storage(mut self, storage_gb: Decimal, retrieval_gb: Decimal) -> Self { self.storage_gb = storage_gb; self.storage_retrieval_gb = retrieval_gb; self } /// Set hosting usage pub fn with_hosting(mut self, bandwidth_gb: Decimal, domains: u32) -> Self { self.hosting_bandwidth_gb = bandwidth_gb; self.custom_domains = domains; self } /// Set database usage pub fn with_database( mut self, storage_gb: Decimal, queries_millions: Decimal, vector_searches_millions: Decimal, ) -> Self { self.database_storage_gb = storage_gb; self.database_queries_millions = queries_millions; self.vector_searches_millions = vector_searches_millions; self } /// Set compute usage pub fn with_compute( mut self, cpu_hours: Decimal, gpu_hours: Decimal, memory_gb_hours: Decimal, invocations_millions: Decimal, ) -> Self { self.cpu_core_hours = cpu_hours; self.gpu_hours = gpu_hours; self.memory_gb_hours = memory_gb_hours; self.invocations_millions = invocations_millions; self } /// Set network usage pub fn with_network(mut self, bandwidth_gb: Decimal) -> Self { self.network_bandwidth_gb = bandwidth_gb; self } /// Set duration pub fn for_months(mut self, months: u32) -> Self { self.duration_months = months.max(1); self } /// Set pricing tier pub fn with_tier(mut self, tier: impl Into) -> Self { self.tier = Some(tier.into()); self } } /// Detailed cost estimate #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CostEstimate { /// Total estimated cost pub total: SynorDecimal, /// Monthly cost (if duration > 1) pub monthly: SynorDecimal, /// Breakdown by service pub by_service: HashMap, /// Free tier savings pub free_tier_savings: SynorDecimal, /// Tier discount applied pub tier_discount: SynorDecimal, /// Tier name used pub tier: String, /// Duration in months pub duration_months: u32, /// USD equivalent (if oracle available) pub usd_equivalent: Option, } /// Cost breakdown for a single service #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServiceCostBreakdown { /// Service type pub service: ServiceType, /// Line items pub items: Vec, /// Subtotal before discounts pub subtotal: SynorDecimal, /// Free tier credit applied pub free_tier_credit: SynorDecimal, /// Net cost pub net_cost: SynorDecimal, } /// Individual cost line item #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CostLineItem { /// Resource description pub description: String, /// Resource unit pub unit: ResourceUnit, /// Quantity pub quantity: Decimal, /// Unit price pub unit_price: SynorDecimal, /// Line total pub total: SynorDecimal, } /// Cost estimator service pub struct CostEstimator { pricing: Arc, } impl CostEstimator { /// Create a new cost estimator pub fn new(pricing: Arc) -> Self { Self { pricing } } /// Estimate cost for a usage projection pub async fn estimate(&self, projection: UsageProjection) -> Result { let tier_name = projection.tier.clone().unwrap_or_else(|| "free".to_string()); let months = projection.duration_months.max(1); let mut by_service = HashMap::new(); let mut total_free_savings = Decimal::ZERO; // Storage costs let storage_breakdown = self.estimate_storage(&projection)?; total_free_savings += storage_breakdown.free_tier_credit; by_service.insert(ServiceType::Storage, storage_breakdown); // Hosting costs let hosting_breakdown = self.estimate_hosting(&projection)?; total_free_savings += hosting_breakdown.free_tier_credit; by_service.insert(ServiceType::Hosting, hosting_breakdown); // Database costs let database_breakdown = self.estimate_database(&projection)?; total_free_savings += database_breakdown.free_tier_credit; by_service.insert(ServiceType::Database, database_breakdown); // Compute costs let compute_breakdown = self.estimate_compute(&projection)?; total_free_savings += compute_breakdown.free_tier_credit; by_service.insert(ServiceType::Compute, compute_breakdown); // Network costs let network_breakdown = self.estimate_network(&projection)?; total_free_savings += network_breakdown.free_tier_credit; by_service.insert(ServiceType::Network, network_breakdown); // Calculate subtotal let subtotal: Decimal = by_service.values().map(|b| b.net_cost).sum(); // Apply tier discount let tier_discount = self.pricing.calculate_tier_discount(&tier_name, subtotal)?; let total = subtotal - tier_discount; // Monthly cost let monthly = total / Decimal::from(months); Ok(CostEstimate { total, monthly, by_service, free_tier_savings: total_free_savings, tier_discount, tier: tier_name, duration_months: months, usd_equivalent: None, // Would come from oracle }) } /// Estimate storage costs fn estimate_storage(&self, projection: &UsageProjection) -> Result { let mut items = Vec::new(); let mut subtotal = Decimal::ZERO; // Storage if projection.storage_gb > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Storage, ResourceUnit::GbMonth) .unwrap_or(Decimal::new(2, 2)); let months = Decimal::from(projection.duration_months.max(1)); let total = projection.storage_gb * price * months; items.push(CostLineItem { description: "Data Storage".to_string(), unit: ResourceUnit::GbMonth, quantity: projection.storage_gb * months, unit_price: price, total, }); subtotal += total; } // Retrieval if projection.storage_retrieval_gb > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Storage, ResourceUnit::BandwidthGb) .unwrap_or(Decimal::new(1, 2)); let total = projection.storage_retrieval_gb * price; items.push(CostLineItem { description: "Data Retrieval".to_string(), unit: ResourceUnit::BandwidthGb, quantity: projection.storage_retrieval_gb, unit_price: price, total, }); subtotal += total; } // Free tier let free_storage = self .pricing .get_free_allocation(ServiceType::Storage, ResourceUnit::GbMonth); let free_credit = (free_storage * Decimal::new(2, 2)).min(subtotal); Ok(ServiceCostBreakdown { service: ServiceType::Storage, items, subtotal, free_tier_credit: free_credit, net_cost: subtotal - free_credit, }) } /// Estimate hosting costs fn estimate_hosting(&self, projection: &UsageProjection) -> Result { let mut items = Vec::new(); let mut subtotal = Decimal::ZERO; // Bandwidth if projection.hosting_bandwidth_gb > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Hosting, ResourceUnit::BandwidthGb) .unwrap_or(Decimal::new(5, 2)); let total = projection.hosting_bandwidth_gb * price; items.push(CostLineItem { description: "Hosting Bandwidth".to_string(), unit: ResourceUnit::BandwidthGb, quantity: projection.hosting_bandwidth_gb, unit_price: price, total, }); subtotal += total; } // Custom domains if projection.custom_domains > 0 { let price = self .pricing .get_base_price(ServiceType::Hosting, ResourceUnit::Domains) .unwrap_or(Decimal::new(50, 2)); let months = Decimal::from(projection.duration_months.max(1)); let total = Decimal::from(projection.custom_domains) * price * months; items.push(CostLineItem { description: "Custom Domains".to_string(), unit: ResourceUnit::Domains, quantity: Decimal::from(projection.custom_domains), unit_price: price, total, }); subtotal += total; } // Free tier let free_bandwidth = self .pricing .get_free_allocation(ServiceType::Hosting, ResourceUnit::BandwidthGb); let free_credit = (free_bandwidth * Decimal::new(5, 2)).min(subtotal); Ok(ServiceCostBreakdown { service: ServiceType::Hosting, items, subtotal, free_tier_credit: free_credit, net_cost: subtotal - free_credit, }) } /// Estimate database costs fn estimate_database(&self, projection: &UsageProjection) -> Result { let mut items = Vec::new(); let mut subtotal = Decimal::ZERO; // Storage if projection.database_storage_gb > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Database, ResourceUnit::GbMonth) .unwrap_or(Decimal::new(10, 2)); let months = Decimal::from(projection.duration_months.max(1)); let total = projection.database_storage_gb * price * months; items.push(CostLineItem { description: "Database Storage".to_string(), unit: ResourceUnit::GbMonth, quantity: projection.database_storage_gb * months, unit_price: price, total, }); subtotal += total; } // Queries if projection.database_queries_millions > Decimal::ZERO { let price_per_query = self .pricing .get_base_price(ServiceType::Database, ResourceUnit::Queries) .unwrap_or(Decimal::new(1, 8)); let queries = projection.database_queries_millions * Decimal::from(1_000_000); let total = queries * price_per_query; items.push(CostLineItem { description: "Database Queries".to_string(), unit: ResourceUnit::Queries, quantity: queries, unit_price: price_per_query, total, }); subtotal += total; } // Vector searches if projection.vector_searches_millions > Decimal::ZERO { let price_per_search = self .pricing .get_base_price(ServiceType::Database, ResourceUnit::VectorSearches) .unwrap_or(Decimal::new(5, 8)); let searches = projection.vector_searches_millions * Decimal::from(1_000_000); let total = searches * price_per_search; items.push(CostLineItem { description: "Vector Searches".to_string(), unit: ResourceUnit::VectorSearches, quantity: searches, unit_price: price_per_search, total, }); subtotal += total; } // Free tier let free_queries = self .pricing .get_free_allocation(ServiceType::Database, ResourceUnit::Queries); let free_credit = (free_queries * Decimal::new(1, 8)).min(subtotal); Ok(ServiceCostBreakdown { service: ServiceType::Database, items, subtotal, free_tier_credit: free_credit, net_cost: subtotal - free_credit, }) } /// Estimate compute costs fn estimate_compute(&self, projection: &UsageProjection) -> Result { let mut items = Vec::new(); let mut subtotal = Decimal::ZERO; // CPU if projection.cpu_core_hours > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Compute, ResourceUnit::CpuCoreHours) .unwrap_or(Decimal::new(2, 2)); let total = projection.cpu_core_hours * price; items.push(CostLineItem { description: "CPU Core-Hours".to_string(), unit: ResourceUnit::CpuCoreHours, quantity: projection.cpu_core_hours, unit_price: price, total, }); subtotal += total; } // GPU if projection.gpu_hours > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Compute, ResourceUnit::GpuHours) .unwrap_or(Decimal::new(50, 2)); let total = projection.gpu_hours * price; items.push(CostLineItem { description: "GPU Hours".to_string(), unit: ResourceUnit::GpuHours, quantity: projection.gpu_hours, unit_price: price, total, }); subtotal += total; } // Memory if projection.memory_gb_hours > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Compute, ResourceUnit::MemoryGbHours) .unwrap_or(Decimal::new(5, 3)); let total = projection.memory_gb_hours * price; items.push(CostLineItem { description: "Memory GB-Hours".to_string(), unit: ResourceUnit::MemoryGbHours, quantity: projection.memory_gb_hours, unit_price: price, total, }); subtotal += total; } // Invocations if projection.invocations_millions > Decimal::ZERO { let price_per_inv = self .pricing .get_base_price(ServiceType::Compute, ResourceUnit::Invocations) .unwrap_or(Decimal::new(2, 7)); let invocations = projection.invocations_millions * Decimal::from(1_000_000); let total = invocations * price_per_inv; items.push(CostLineItem { description: "Serverless Invocations".to_string(), unit: ResourceUnit::Invocations, quantity: invocations, unit_price: price_per_inv, total, }); subtotal += total; } // Free tier let free_cpu = self .pricing .get_free_allocation(ServiceType::Compute, ResourceUnit::CpuCoreHours); let free_credit = (free_cpu * Decimal::new(2, 2)).min(subtotal); Ok(ServiceCostBreakdown { service: ServiceType::Compute, items, subtotal, free_tier_credit: free_credit, net_cost: subtotal - free_credit, }) } /// Estimate network costs fn estimate_network(&self, projection: &UsageProjection) -> Result { let mut items = Vec::new(); let mut subtotal = Decimal::ZERO; if projection.network_bandwidth_gb > Decimal::ZERO { let price = self .pricing .get_base_price(ServiceType::Network, ResourceUnit::BandwidthGb) .unwrap_or(Decimal::new(1, 2)); let total = projection.network_bandwidth_gb * price; items.push(CostLineItem { description: "Network Bandwidth".to_string(), unit: ResourceUnit::BandwidthGb, quantity: projection.network_bandwidth_gb, unit_price: price, total, }); subtotal += total; } // Free tier let free_bandwidth = self .pricing .get_free_allocation(ServiceType::Network, ResourceUnit::BandwidthGb); let free_credit = (free_bandwidth * Decimal::new(1, 2)).min(subtotal); Ok(ServiceCostBreakdown { service: ServiceType::Network, items, subtotal, free_tier_credit: free_credit, net_cost: subtotal - free_credit, }) } /// Quick estimate for a single service pub fn quick_estimate( &self, service: ServiceType, resource: ResourceUnit, amount: Decimal, ) -> Result { self.pricing.calculate_cost(service, resource, amount) } } #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; fn setup_estimator() -> CostEstimator { let pricing = Arc::new(PricingEngine::new()); CostEstimator::new(pricing) } #[tokio::test] async fn test_storage_estimate() { let estimator = setup_estimator(); let projection = UsageProjection::new() .with_storage(dec!(100), dec!(50)) // 100 GB storage, 50 GB retrieval .for_months(1); let estimate = estimator.estimate(projection).await.unwrap(); // Storage: 100 * 0.02 = 2.00 // Retrieval: 50 * 0.01 = 0.50 // Total before free tier: 2.50 assert!(estimate.total > Decimal::ZERO); assert!(estimate.by_service.contains_key(&ServiceType::Storage)); } #[tokio::test] async fn test_compute_estimate() { let estimator = setup_estimator(); let projection = UsageProjection::new() .with_compute(dec!(100), dec!(10), dec!(200), dec!(5)) // 100 CPU hours, 10 GPU hours, 200 mem hours, 5M invocations .for_months(1); let estimate = estimator.estimate(projection).await.unwrap(); // CPU: 100 * 0.02 = 2.00 // GPU: 10 * 0.50 = 5.00 // Memory: 200 * 0.005 = 1.00 // Invocations: 5M * 0.0000002 = 1.00 // Total: ~9.00 before free tier assert!(estimate.by_service.contains_key(&ServiceType::Compute)); let compute = &estimate.by_service[&ServiceType::Compute]; assert_eq!(compute.items.len(), 4); } #[tokio::test] async fn test_tier_discount() { let estimator = setup_estimator(); let projection = UsageProjection::new() .with_storage(dec!(1000), dec!(500)) .with_tier("premium"); // 20% discount let estimate = estimator.estimate(projection).await.unwrap(); assert_eq!(estimate.tier, "premium"); assert!(estimate.tier_discount > Decimal::ZERO); } #[test] fn test_quick_estimate() { let estimator = setup_estimator(); let cost = estimator .quick_estimate(ServiceType::Compute, ResourceUnit::GpuHours, dec!(10)) .unwrap(); assert_eq!(cost, dec!(5.00)); // 10 * 0.50 } }