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.
649 lines
21 KiB
Rust
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
|
|
}
|
|
}
|