Files
rustytorch/crates/specialized/rtx-platform/tests/billing_manager_tests.rs
T
2026-03-04 00:08:42 +00:00

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(())
}