synor/crates/synor-economics/src/calculator/estimator.rs
Gulshan Yadav 17f0b4ce4b feat(economics): add Phase 12 - Economics & Billing infrastructure
Complete economics service implementation with:

- Price Oracle with TWAP (Time-Weighted Average Price)
  - Multi-source price aggregation
  - Configurable staleness thresholds
  - Exponential, SMA, and standard TWAP strategies

- Metering Service for L2 usage tracking
  - Storage: GB-months, retrieval bandwidth
  - Hosting: bandwidth, custom domains
  - Database: queries, vector searches
  - Compute: CPU core-hours, GPU hours, memory
  - Network: bandwidth, requests

- Billing Engine
  - Invoice generation with line items
  - Payment processing (crypto/fiat)
  - Credit management with expiration
  - Auto-pay from prepaid balance

- Pricing Tiers: Free, Standard, Premium, Enterprise
  - 0%, 10%, 20%, 30% usage discounts
  - SLA guarantees: 95%, 99%, 99.9%, 99.99%

- Cost Calculator & Estimator
  - Usage projections
  - Tier comparison recommendations
  - ROI analysis

- Docker deployment with PostgreSQL schema

All 61 tests passing.
2026-01-19 21:51:26 +05:30

649 lines
21 KiB
Rust

//! 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<String>,
}
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<String>) -> 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<ServiceType, ServiceCostBreakdown>,
/// 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<Decimal>,
}
/// Cost breakdown for a single service
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceCostBreakdown {
/// Service type
pub service: ServiceType,
/// Line items
pub items: Vec<CostLineItem>,
/// 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<PricingEngine>,
}
impl CostEstimator {
/// Create a new cost estimator
pub fn new(pricing: Arc<PricingEngine>) -> Self {
Self { pricing }
}
/// Estimate cost for a usage projection
pub async fn estimate(&self, projection: UsageProjection) -> Result<CostEstimate> {
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<ServiceCostBreakdown> {
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<ServiceCostBreakdown> {
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<ServiceCostBreakdown> {
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<ServiceCostBreakdown> {
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<ServiceCostBreakdown> {
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<SynorDecimal> {
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
}
}