455 lines
14 KiB
Rust
455 lines
14 KiB
Rust
//! STRICT TDD - Billing Manager Tests
|
|
//! These tests define the complete billing system behavior before implementation
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use std::collections::HashMap;
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
use tokio::time;
|
|
use uuid::Uuid;
|
|
|
|
use rtx_platform::billing::{
|
|
AggregationPeriod, AlertThreshold, BillingAlert, BillingConfig, BillingManager, CostCalculator,
|
|
Invoice, InvoiceGenerator, PricingTier, ResourceType, UsageMeter, UsageRecord,
|
|
};
|
|
use rtx_platform::{PlatformConfig, PlatformResult};
|
|
|
|
fn create_test_billing_config() -> BillingConfig {
|
|
BillingConfig {
|
|
pricing_tiers: vec![
|
|
PricingTier {
|
|
resource_type: ResourceType::Compute,
|
|
tiers: vec![
|
|
(0.0, 1000.0, 0.10), // $0.10/hour for first 1000 hours
|
|
(1000.0, 5000.0, 0.08), // $0.08/hour for next 4000 hours
|
|
(5000.0, f64::INFINITY, 0.06), // $0.06/hour for 5000+ hours
|
|
],
|
|
},
|
|
PricingTier {
|
|
resource_type: ResourceType::Storage,
|
|
tiers: vec![
|
|
(0.0, 1024.0, 0.023), // $0.023/GB-month for first TB
|
|
(1024.0, f64::INFINITY, 0.021), // $0.021/GB-month for 1TB+
|
|
],
|
|
},
|
|
PricingTier {
|
|
resource_type: ResourceType::NetworkEgress,
|
|
tiers: vec![
|
|
(0.0, 100.0, 0.0), // First 100GB free
|
|
(100.0, 1024.0, 0.09), // $0.09/GB for next 924GB
|
|
(1024.0, f64::INFINITY, 0.085), // $0.085/GB for 1TB+
|
|
],
|
|
},
|
|
],
|
|
aggregation_period: AggregationPeriod::Hourly,
|
|
alert_thresholds: vec![AlertThreshold {
|
|
tenant_id: "default".to_string(),
|
|
monthly_limit: 1000.0,
|
|
warning_threshold: 0.8,
|
|
critical_threshold: 0.95,
|
|
}],
|
|
invoice_generation_day: 1,
|
|
late_fee_rate: 0.015, // 1.5% monthly late fee
|
|
}
|
|
}
|
|
|
|
fn create_test_platform_config() -> PlatformConfig {
|
|
PlatformConfig {
|
|
database_url: "postgresql://test:test@localhost/billing_test".to_string(),
|
|
redis_urls: vec!["redis://localhost:6379".to_string()],
|
|
kafka_brokers: vec!["localhost:9092".to_string()],
|
|
metrics_endpoint: "localhost:9090".to_string(),
|
|
regions: HashMap::new(),
|
|
sla_targets: rtx_platform::slo::SlaTargets::default(),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_billing_manager_creation() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
|
|
assert!(billing_manager.is_healthy().await?);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_usage_meter_records_compute_usage() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
let region = "us-west-2".to_string();
|
|
|
|
// Record compute usage
|
|
let usage_record = UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: region.clone(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 2.5, // 2.5 GPU hours
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::from([
|
|
("instance_type".to_string(), "h100-8x".to_string()),
|
|
("region".to_string(), region),
|
|
]),
|
|
};
|
|
|
|
billing_manager.record_usage(usage_record.clone()).await?;
|
|
|
|
// Verify usage was recorded
|
|
let recorded_usage = billing_manager
|
|
.get_usage_for_tenant(tenant_id, Utc::now().date_naive())
|
|
.await?;
|
|
|
|
assert_eq!(recorded_usage.len(), 1);
|
|
assert_eq!(recorded_usage[0].resource_type, ResourceType::Compute);
|
|
assert_eq!(recorded_usage[0].quantity, 2.5);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cost_calculation_tiered_pricing() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
let cost_calculator = billing_manager.cost_calculator();
|
|
|
|
// Test tiered compute pricing
|
|
let usage_records = vec![
|
|
UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id: Uuid::new_v4(),
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 500.0, // First tier: 500 * $0.10 = $50.00
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
},
|
|
UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id: Uuid::new_v4(),
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 1500.0, // Tiered: 1000 * $0.10 + 500 * $0.08 = $140.00
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
},
|
|
];
|
|
|
|
let total_cost = cost_calculator.calculate_cost(&usage_records).await?;
|
|
|
|
// Expected: $50.00 (500 * $0.10) + $140.00 (1000 * $0.10 + 500 * $0.08) = $190.00
|
|
assert_eq!((total_cost * 100.0).round() / 100.0, 190.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cost_calculation_free_tier() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
let cost_calculator = billing_manager.cost_calculator();
|
|
|
|
// Test free network egress tier
|
|
let usage_records = vec![UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id: Uuid::new_v4(),
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::NetworkEgress,
|
|
quantity: 50.0, // First 100GB free
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
}];
|
|
|
|
let total_cost = cost_calculator.calculate_cost(&usage_records).await?;
|
|
|
|
// Should be $0.00 for free tier
|
|
assert_eq!(total_cost, 0.0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_real_time_billing_alerts() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let mut billing_config = create_test_billing_config();
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
billing_config.alert_thresholds[0].tenant_id = tenant_id.to_string();
|
|
billing_config.alert_thresholds[0].monthly_limit = 100.0; // Low limit for testing
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
// Record usage that exceeds warning threshold (80% of $100 = $80)
|
|
let high_usage = UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 850.0, // 850 * $0.10 = $85.00 (exceeds warning)
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
billing_manager.record_usage(high_usage).await?;
|
|
|
|
// Wait for alert processing
|
|
time::sleep(Duration::from_millis(100)).await;
|
|
|
|
let alerts = billing_manager.get_active_alerts(tenant_id).await?;
|
|
|
|
assert!(!alerts.is_empty());
|
|
assert_eq!(alerts[0].alert_type, BillingAlert::Warning);
|
|
assert_eq!(alerts[0].tenant_id, tenant_id);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_usage_aggregation_across_regions() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
|
|
// Record usage across multiple regions
|
|
let usage_records = vec![
|
|
UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 100.0,
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
},
|
|
UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "us-east-1".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 150.0,
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
},
|
|
UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "eu-west-1".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 200.0,
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
},
|
|
];
|
|
|
|
for record in usage_records {
|
|
billing_manager.record_usage(record).await?;
|
|
}
|
|
|
|
let aggregated_usage = billing_manager
|
|
.get_aggregated_usage(tenant_id, AggregationPeriod::Daily)
|
|
.await?;
|
|
|
|
// Should aggregate to 450.0 total across all regions
|
|
let total_compute = aggregated_usage
|
|
.get(&ResourceType::Compute)
|
|
.expect("Compute usage should be present");
|
|
|
|
assert_eq!(*total_compute, 450.0);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_invoice_generation_monthly() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
|
|
// Record usage for the month
|
|
let usage_record = UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: 1000.0, // $100.00
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
billing_manager.record_usage(usage_record).await?;
|
|
|
|
// Generate invoice
|
|
let invoice = billing_manager
|
|
.generate_invoice(tenant_id, Utc::now().date_naive())
|
|
.await?;
|
|
|
|
assert_eq!(invoice.tenant_id, tenant_id);
|
|
assert_eq!((invoice.total_amount * 100.0).round() / 100.0, 100.0);
|
|
assert_eq!(invoice.line_items.len(), 1);
|
|
assert_eq!(invoice.line_items[0].resource_type, ResourceType::Compute);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_invoice_late_fees() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
|
|
// Create overdue invoice (30+ days old)
|
|
let old_date = Utc::now().date_naive() - chrono::Duration::days(35);
|
|
let mut invoice = Invoice {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
billing_period_start: old_date,
|
|
billing_period_end: old_date + chrono::Duration::days(30),
|
|
generated_at: DateTime::from_naive_utc_and_offset(
|
|
old_date.and_hms_opt(0, 0, 0).unwrap(),
|
|
Utc,
|
|
),
|
|
due_date: old_date + chrono::Duration::days(30),
|
|
total_amount: 100.0,
|
|
line_items: vec![],
|
|
status: rtx_platform::billing::InvoiceStatus::Outstanding,
|
|
late_fees: 0.0,
|
|
};
|
|
|
|
billing_manager.store_invoice(&invoice).await?;
|
|
|
|
// Process late fees
|
|
billing_manager.process_late_fees().await?;
|
|
|
|
// Retrieve updated invoice
|
|
let updated_invoice = billing_manager.get_invoice(invoice.id).await?;
|
|
|
|
// Should have 1.5% late fee
|
|
let expected_late_fee = 100.0 * 0.015;
|
|
assert_eq!(
|
|
(updated_invoice.late_fees * 1000.0).round() / 1000.0,
|
|
expected_late_fee
|
|
);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_billing_precision_tracking() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
|
|
// Record precise usage with fractional values
|
|
let precision_usage = UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Storage,
|
|
quantity: 1536.7832, // Precise GB-months
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
billing_manager.record_usage(precision_usage).await?;
|
|
|
|
let cost_calculator = billing_manager.cost_calculator();
|
|
let usage_records = billing_manager
|
|
.get_usage_for_tenant(tenant_id, Utc::now().date_naive())
|
|
.await?;
|
|
|
|
let total_cost = cost_calculator.calculate_cost(&usage_records).await?;
|
|
|
|
// Verify precision is maintained in calculations
|
|
// First 1024GB at $0.023 + 512.7832GB at $0.021
|
|
let expected_cost = (1024.0 * 0.023) + (512.7832 * 0.021);
|
|
let cost_diff = (total_cost - expected_cost).abs();
|
|
|
|
assert!(
|
|
cost_diff < 0.0001,
|
|
"Cost calculation should be precise to 4 decimal places"
|
|
);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_usage_recording() -> PlatformResult<()> {
|
|
let config = create_test_platform_config();
|
|
let billing_config = create_test_billing_config();
|
|
|
|
let mut billing_manager = BillingManager::new(&config, billing_config).await?;
|
|
billing_manager.start().await?;
|
|
|
|
let tenant_id = Uuid::new_v4();
|
|
|
|
// Spawn multiple concurrent usage recording tasks
|
|
let mut handles = vec![];
|
|
|
|
for i in 0..100 {
|
|
let billing_manager_clone = billing_manager.clone();
|
|
let handle = tokio::spawn(async move {
|
|
let usage_record = UsageRecord {
|
|
id: Uuid::new_v4(),
|
|
tenant_id,
|
|
region: "us-west-2".to_string(),
|
|
resource_type: ResourceType::Compute,
|
|
quantity: i as f64 * 0.1,
|
|
timestamp: Utc::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
billing_manager_clone.record_usage(usage_record).await
|
|
});
|
|
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Wait for all tasks to complete
|
|
for handle in handles {
|
|
handle.await.unwrap()?;
|
|
}
|
|
|
|
let usage_records = billing_manager
|
|
.get_usage_for_tenant(tenant_id, Utc::now().date_naive())
|
|
.await?;
|
|
|
|
// All 100 records should be stored
|
|
assert_eq!(usage_records.len(), 100);
|
|
|
|
billing_manager.shutdown().await?;
|
|
Ok(())
|
|
}
|