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.
463 lines
16 KiB
Rust
463 lines
16 KiB
Rust
//! Pricing Engine
|
|
//!
|
|
//! Service pricing, tier management, and discount system.
|
|
|
|
mod discounts;
|
|
mod tiers;
|
|
|
|
pub use discounts::{Discount, DiscountType};
|
|
pub use tiers::PricingTier;
|
|
|
|
use crate::error::{EconomicsError, Result};
|
|
use crate::{ResourceUnit, ServiceType, SynorDecimal};
|
|
use rust_decimal::Decimal;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
/// Service pricing configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServicePricing {
|
|
/// Base prices by resource unit
|
|
pub base_prices: HashMap<ResourceUnit, SynorDecimal>,
|
|
/// Minimum billable amount
|
|
pub minimum_charge: SynorDecimal,
|
|
/// Free tier included
|
|
pub free_tier: Option<FreeTier>,
|
|
}
|
|
|
|
/// Free tier allocation
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FreeTier {
|
|
/// Free allocation per resource
|
|
pub allocations: HashMap<ResourceUnit, Decimal>,
|
|
/// Monthly reset
|
|
pub monthly_reset: bool,
|
|
}
|
|
|
|
/// Pricing engine managing all pricing logic
|
|
pub struct PricingEngine {
|
|
/// Pricing by service type
|
|
service_pricing: HashMap<ServiceType, ServicePricing>,
|
|
/// Available pricing tiers
|
|
tiers: Vec<PricingTier>,
|
|
/// Active discounts
|
|
discounts: Vec<Discount>,
|
|
}
|
|
|
|
impl PricingEngine {
|
|
/// Create a new pricing engine with default prices
|
|
pub fn new() -> Self {
|
|
let mut engine = Self {
|
|
service_pricing: HashMap::new(),
|
|
tiers: Vec::new(),
|
|
discounts: Vec::new(),
|
|
};
|
|
|
|
engine.initialize_default_pricing();
|
|
engine.initialize_default_tiers();
|
|
|
|
engine
|
|
}
|
|
|
|
/// Initialize default service pricing
|
|
fn initialize_default_pricing(&mut self) {
|
|
// Storage L2 pricing
|
|
let mut storage_prices = HashMap::new();
|
|
storage_prices.insert(ResourceUnit::GbMonth, Decimal::new(2, 2)); // 0.02 SYNOR/GB/month
|
|
storage_prices.insert(ResourceUnit::BandwidthGb, Decimal::new(1, 2)); // 0.01 SYNOR/GB retrieval
|
|
storage_prices.insert(ResourceUnit::Bytes, Decimal::new(2, 11)); // For small files
|
|
|
|
self.service_pricing.insert(
|
|
ServiceType::Storage,
|
|
ServicePricing {
|
|
base_prices: storage_prices,
|
|
minimum_charge: Decimal::new(1, 4), // 0.0001 SYNOR
|
|
free_tier: Some(FreeTier {
|
|
allocations: [(ResourceUnit::GbMonth, Decimal::new(5, 1))].into(), // 0.5 GB free
|
|
monthly_reset: true,
|
|
}),
|
|
},
|
|
);
|
|
|
|
// Hosting pricing
|
|
let mut hosting_prices = HashMap::new();
|
|
hosting_prices.insert(ResourceUnit::BandwidthGb, Decimal::new(5, 2)); // 0.05 SYNOR/GB
|
|
hosting_prices.insert(ResourceUnit::Domains, Decimal::new(50, 2)); // 0.50 SYNOR/domain/month
|
|
|
|
self.service_pricing.insert(
|
|
ServiceType::Hosting,
|
|
ServicePricing {
|
|
base_prices: hosting_prices,
|
|
minimum_charge: Decimal::ZERO,
|
|
free_tier: Some(FreeTier {
|
|
allocations: [
|
|
(ResourceUnit::BandwidthGb, Decimal::new(1, 0)), // 1 GB free
|
|
]
|
|
.into(),
|
|
monthly_reset: true,
|
|
}),
|
|
},
|
|
);
|
|
|
|
// Database L2 pricing
|
|
let mut db_prices = HashMap::new();
|
|
db_prices.insert(ResourceUnit::GbMonth, Decimal::new(10, 2)); // 0.10 SYNOR/GB/month
|
|
db_prices.insert(ResourceUnit::Queries, Decimal::new(1, 8)); // 0.00000001 SYNOR/query (0.01/million)
|
|
db_prices.insert(ResourceUnit::VectorSearches, Decimal::new(5, 8)); // 0.00000005 SYNOR/search (0.05/million)
|
|
|
|
self.service_pricing.insert(
|
|
ServiceType::Database,
|
|
ServicePricing {
|
|
base_prices: db_prices,
|
|
minimum_charge: Decimal::new(1, 4),
|
|
free_tier: Some(FreeTier {
|
|
allocations: [
|
|
(ResourceUnit::GbMonth, Decimal::new(5, 1)), // 0.5 GB free
|
|
(ResourceUnit::Queries, Decimal::new(1_000_000, 0)), // 1M queries free
|
|
(ResourceUnit::VectorSearches, Decimal::new(100_000, 0)), // 100K vector searches free
|
|
]
|
|
.into(),
|
|
monthly_reset: true,
|
|
}),
|
|
},
|
|
);
|
|
|
|
// Compute L2 pricing
|
|
let mut compute_prices = HashMap::new();
|
|
compute_prices.insert(ResourceUnit::CpuCoreHours, Decimal::new(2, 2)); // 0.02 SYNOR/core-hour
|
|
compute_prices.insert(ResourceUnit::GpuHours, Decimal::new(50, 2)); // 0.50 SYNOR/GPU-hour (RTX 4090)
|
|
compute_prices.insert(ResourceUnit::MemoryGbHours, Decimal::new(5, 3)); // 0.005 SYNOR/GB-hour
|
|
compute_prices.insert(ResourceUnit::Invocations, Decimal::new(2, 7)); // 0.0000002 SYNOR/invocation (0.20/million)
|
|
|
|
self.service_pricing.insert(
|
|
ServiceType::Compute,
|
|
ServicePricing {
|
|
base_prices: compute_prices,
|
|
minimum_charge: Decimal::new(1, 4),
|
|
free_tier: Some(FreeTier {
|
|
allocations: [
|
|
(ResourceUnit::CpuCoreHours, Decimal::new(100, 0)), // 100 core-hours free
|
|
(ResourceUnit::MemoryGbHours, Decimal::new(200, 0)), // 200 GB-hours free
|
|
(ResourceUnit::Invocations, Decimal::new(1_000_000, 0)), // 1M invocations free
|
|
]
|
|
.into(),
|
|
monthly_reset: true,
|
|
}),
|
|
},
|
|
);
|
|
|
|
// Network pricing
|
|
let mut network_prices = HashMap::new();
|
|
network_prices.insert(ResourceUnit::BandwidthGb, Decimal::new(1, 2)); // 0.01 SYNOR/GB
|
|
|
|
self.service_pricing.insert(
|
|
ServiceType::Network,
|
|
ServicePricing {
|
|
base_prices: network_prices,
|
|
minimum_charge: Decimal::ZERO,
|
|
free_tier: Some(FreeTier {
|
|
allocations: [(ResourceUnit::BandwidthGb, Decimal::new(10, 0))].into(), // 10 GB free
|
|
monthly_reset: true,
|
|
}),
|
|
},
|
|
);
|
|
|
|
// Contract execution (gas pricing)
|
|
let mut contract_prices = HashMap::new();
|
|
contract_prices.insert(ResourceUnit::Gas, Decimal::new(1, 9)); // 0.000000001 SYNOR/gas
|
|
|
|
self.service_pricing.insert(
|
|
ServiceType::Contract,
|
|
ServicePricing {
|
|
base_prices: contract_prices,
|
|
minimum_charge: Decimal::ZERO,
|
|
free_tier: None,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Initialize default pricing tiers
|
|
fn initialize_default_tiers(&mut self) {
|
|
self.tiers = vec![
|
|
PricingTier::free(),
|
|
PricingTier::standard(),
|
|
PricingTier::premium(),
|
|
PricingTier::enterprise(),
|
|
];
|
|
}
|
|
|
|
/// Calculate cost for resource usage
|
|
pub fn calculate_cost(
|
|
&self,
|
|
service_type: ServiceType,
|
|
resource_unit: ResourceUnit,
|
|
amount: Decimal,
|
|
) -> Result<SynorDecimal> {
|
|
let pricing = self
|
|
.service_pricing
|
|
.get(&service_type)
|
|
.ok_or_else(|| EconomicsError::ServiceNotConfigured(service_type.to_string()))?;
|
|
|
|
let unit_price = pricing
|
|
.base_prices
|
|
.get(&resource_unit)
|
|
.ok_or_else(|| {
|
|
EconomicsError::ServiceNotConfigured(format!(
|
|
"{} - {}",
|
|
service_type, resource_unit
|
|
))
|
|
})?;
|
|
|
|
let cost = amount * unit_price;
|
|
|
|
// Apply minimum charge
|
|
Ok(cost.max(pricing.minimum_charge))
|
|
}
|
|
|
|
/// Calculate cost with tier discount applied
|
|
pub fn calculate_cost_with_tier(
|
|
&self,
|
|
service_type: ServiceType,
|
|
resource_unit: ResourceUnit,
|
|
amount: Decimal,
|
|
tier_name: &str,
|
|
) -> Result<SynorDecimal> {
|
|
let base_cost = self.calculate_cost(service_type, resource_unit, amount)?;
|
|
|
|
let tier = self.tiers.iter().find(|t| t.name == tier_name);
|
|
|
|
if let Some(tier) = tier {
|
|
let discount_rate = tier.discount_percentage / Decimal::ONE_HUNDRED;
|
|
Ok(base_cost * (Decimal::ONE - discount_rate))
|
|
} else {
|
|
Ok(base_cost)
|
|
}
|
|
}
|
|
|
|
/// Calculate tier discount amount
|
|
pub fn calculate_tier_discount(
|
|
&self,
|
|
tier_name: &str,
|
|
subtotal: SynorDecimal,
|
|
) -> Result<SynorDecimal> {
|
|
let tier = self.tiers.iter().find(|t| t.name == tier_name);
|
|
|
|
if let Some(tier) = tier {
|
|
let discount_rate = tier.discount_percentage / Decimal::ONE_HUNDRED;
|
|
Ok(subtotal * discount_rate)
|
|
} else {
|
|
Ok(Decimal::ZERO)
|
|
}
|
|
}
|
|
|
|
/// Get free tier allocation for a resource
|
|
pub fn get_free_allocation(
|
|
&self,
|
|
service_type: ServiceType,
|
|
resource_unit: ResourceUnit,
|
|
) -> Decimal {
|
|
self.service_pricing
|
|
.get(&service_type)
|
|
.and_then(|p| p.free_tier.as_ref())
|
|
.and_then(|f| f.allocations.get(&resource_unit))
|
|
.copied()
|
|
.unwrap_or(Decimal::ZERO)
|
|
}
|
|
|
|
/// Get base price for a resource
|
|
pub fn get_base_price(
|
|
&self,
|
|
service_type: ServiceType,
|
|
resource_unit: ResourceUnit,
|
|
) -> Option<SynorDecimal> {
|
|
self.service_pricing
|
|
.get(&service_type)
|
|
.and_then(|p| p.base_prices.get(&resource_unit))
|
|
.copied()
|
|
}
|
|
|
|
/// Get pricing tier by name
|
|
pub fn get_tier(&self, name: &str) -> Option<&PricingTier> {
|
|
self.tiers.iter().find(|t| t.name == name)
|
|
}
|
|
|
|
/// Get all tiers
|
|
pub fn get_all_tiers(&self) -> &[PricingTier] {
|
|
&self.tiers
|
|
}
|
|
|
|
/// Add a custom discount
|
|
pub fn add_discount(&mut self, discount: Discount) {
|
|
self.discounts.push(discount);
|
|
}
|
|
|
|
/// Apply applicable discounts to an amount
|
|
pub fn apply_discounts(
|
|
&self,
|
|
amount: SynorDecimal,
|
|
account_id: &str,
|
|
service_type: Option<ServiceType>,
|
|
) -> (SynorDecimal, Vec<Discount>) {
|
|
let mut discounted = amount;
|
|
let mut applied = Vec::new();
|
|
|
|
for discount in &self.discounts {
|
|
if discount.is_applicable(account_id, service_type) {
|
|
let discount_amount = discount.calculate_discount(discounted);
|
|
discounted -= discount_amount;
|
|
applied.push(discount.clone());
|
|
}
|
|
}
|
|
|
|
(discounted, applied)
|
|
}
|
|
|
|
/// Get pricing summary for display
|
|
pub fn get_pricing_summary(&self) -> PricingSummary {
|
|
PricingSummary {
|
|
storage: ServicePricingSummary {
|
|
gb_month: self.get_base_price(ServiceType::Storage, ResourceUnit::GbMonth),
|
|
retrieval_gb: self.get_base_price(ServiceType::Storage, ResourceUnit::BandwidthGb),
|
|
free_storage_gb: self.get_free_allocation(ServiceType::Storage, ResourceUnit::GbMonth),
|
|
},
|
|
hosting: HostingPricingSummary {
|
|
bandwidth_gb: self.get_base_price(ServiceType::Hosting, ResourceUnit::BandwidthGb),
|
|
domain_month: self.get_base_price(ServiceType::Hosting, ResourceUnit::Domains),
|
|
free_bandwidth_gb: self
|
|
.get_free_allocation(ServiceType::Hosting, ResourceUnit::BandwidthGb),
|
|
},
|
|
database: DatabasePricingSummary {
|
|
storage_gb_month: self.get_base_price(ServiceType::Database, ResourceUnit::GbMonth),
|
|
queries_million: self
|
|
.get_base_price(ServiceType::Database, ResourceUnit::Queries)
|
|
.map(|p| p * Decimal::from(1_000_000)),
|
|
vector_searches_million: self
|
|
.get_base_price(ServiceType::Database, ResourceUnit::VectorSearches)
|
|
.map(|p| p * Decimal::from(1_000_000)),
|
|
free_queries: self
|
|
.get_free_allocation(ServiceType::Database, ResourceUnit::Queries),
|
|
},
|
|
compute: ComputePricingSummary {
|
|
cpu_core_hour: self.get_base_price(ServiceType::Compute, ResourceUnit::CpuCoreHours),
|
|
gpu_hour: self.get_base_price(ServiceType::Compute, ResourceUnit::GpuHours),
|
|
memory_gb_hour: self
|
|
.get_base_price(ServiceType::Compute, ResourceUnit::MemoryGbHours),
|
|
invocations_million: self
|
|
.get_base_price(ServiceType::Compute, ResourceUnit::Invocations)
|
|
.map(|p| p * Decimal::from(1_000_000)),
|
|
free_cpu_hours: self
|
|
.get_free_allocation(ServiceType::Compute, ResourceUnit::CpuCoreHours),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for PricingEngine {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Pricing summary for display
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PricingSummary {
|
|
pub storage: ServicePricingSummary,
|
|
pub hosting: HostingPricingSummary,
|
|
pub database: DatabasePricingSummary,
|
|
pub compute: ComputePricingSummary,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServicePricingSummary {
|
|
pub gb_month: Option<SynorDecimal>,
|
|
pub retrieval_gb: Option<SynorDecimal>,
|
|
pub free_storage_gb: SynorDecimal,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct HostingPricingSummary {
|
|
pub bandwidth_gb: Option<SynorDecimal>,
|
|
pub domain_month: Option<SynorDecimal>,
|
|
pub free_bandwidth_gb: SynorDecimal,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DatabasePricingSummary {
|
|
pub storage_gb_month: Option<SynorDecimal>,
|
|
pub queries_million: Option<SynorDecimal>,
|
|
pub vector_searches_million: Option<SynorDecimal>,
|
|
pub free_queries: SynorDecimal,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ComputePricingSummary {
|
|
pub cpu_core_hour: Option<SynorDecimal>,
|
|
pub gpu_hour: Option<SynorDecimal>,
|
|
pub memory_gb_hour: Option<SynorDecimal>,
|
|
pub invocations_million: Option<SynorDecimal>,
|
|
pub free_cpu_hours: SynorDecimal,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use rust_decimal_macros::dec;
|
|
|
|
#[test]
|
|
fn test_storage_pricing() {
|
|
let engine = PricingEngine::new();
|
|
|
|
// 10 GB storage for a month
|
|
let cost = engine
|
|
.calculate_cost(ServiceType::Storage, ResourceUnit::GbMonth, dec!(10))
|
|
.unwrap();
|
|
|
|
assert_eq!(cost, dec!(0.20)); // 10 * 0.02
|
|
}
|
|
|
|
#[test]
|
|
fn test_compute_pricing() {
|
|
let engine = PricingEngine::new();
|
|
|
|
// 5 GPU hours
|
|
let cost = engine
|
|
.calculate_cost(ServiceType::Compute, ResourceUnit::GpuHours, dec!(5))
|
|
.unwrap();
|
|
|
|
assert_eq!(cost, dec!(2.50)); // 5 * 0.50
|
|
}
|
|
|
|
#[test]
|
|
fn test_database_queries() {
|
|
let engine = PricingEngine::new();
|
|
|
|
// 10 million queries
|
|
let cost = engine
|
|
.calculate_cost(ServiceType::Database, ResourceUnit::Queries, dec!(10_000_000))
|
|
.unwrap();
|
|
|
|
assert_eq!(cost, dec!(0.10)); // 10M * 0.00000001
|
|
}
|
|
|
|
#[test]
|
|
fn test_tier_discount() {
|
|
let engine = PricingEngine::new();
|
|
|
|
// Premium tier gets 20% discount
|
|
let base_cost = dec!(100);
|
|
let discount = engine.calculate_tier_discount("premium", base_cost).unwrap();
|
|
|
|
assert_eq!(discount, dec!(20)); // 20%
|
|
}
|
|
|
|
#[test]
|
|
fn test_free_allocation() {
|
|
let engine = PricingEngine::new();
|
|
|
|
let free_storage = engine.get_free_allocation(ServiceType::Storage, ResourceUnit::GbMonth);
|
|
assert_eq!(free_storage, dec!(0.5));
|
|
|
|
let free_queries = engine.get_free_allocation(ServiceType::Database, ResourceUnit::Queries);
|
|
assert_eq!(free_queries, dec!(1_000_000));
|
|
}
|
|
}
|