Skip to main content

oxirs_vec/multi_tenancy/
billing.rs

1//! Billing and usage metering for multi-tenancy
2
3use crate::multi_tenancy::types::{MultiTenancyError, MultiTenancyResult, TenantOperation};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9/// Billing period for charges
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum BillingPeriod {
12    Hourly,
13    Daily,
14    Monthly,
15    Annual,
16}
17
18impl BillingPeriod {
19    /// Get duration in seconds
20    pub fn duration_secs(&self) -> i64 {
21        match self {
22            Self::Hourly => 3600,
23            Self::Daily => 86400,
24            Self::Monthly => 2592000, // 30 days
25            Self::Annual => 31536000, // 365 days
26        }
27    }
28}
29
30/// Pricing model for billing
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum PricingModel {
33    /// Pay per request
34    PerRequest {
35        /// Cost per request
36        cost_per_request: f64,
37    },
38    /// Pay per vector stored
39    PerVector {
40        /// Cost per 1000 vectors per month
41        cost_per_1k_vectors: f64,
42    },
43    /// Pay per storage GB
44    PerStorage {
45        /// Cost per GB per month
46        cost_per_gb: f64,
47    },
48    /// Pay per compute unit
49    PerComputeUnit {
50        /// Cost per compute unit (query complexity weighted)
51        cost_per_unit: f64,
52    },
53    /// Flat subscription
54    Subscription {
55        /// Monthly subscription fee
56        monthly_fee: f64,
57        /// Included requests
58        included_requests: u64,
59        /// Overage cost per request
60        overage_cost: f64,
61    },
62    /// Custom pricing
63    Custom {
64        /// Base fee
65        base_fee: f64,
66        /// Operation costs
67        operation_costs: HashMap<String, f64>,
68    },
69}
70
71impl PricingModel {
72    /// Calculate cost for an operation
73    pub fn calculate_cost(&self, operation: TenantOperation, count: u64) -> f64 {
74        match self {
75            Self::PerRequest { cost_per_request } => *cost_per_request * count as f64,
76            Self::PerComputeUnit { cost_per_unit } => {
77                *cost_per_unit * operation.default_cost_weight() * count as f64
78            }
79            Self::Custom {
80                operation_costs, ..
81            } => {
82                let op_cost = operation_costs
83                    .get(operation.name())
84                    .copied()
85                    .unwrap_or(0.01);
86                op_cost * count as f64
87            }
88            _ => 0.0, // Other models calculated differently
89        }
90    }
91}
92
93/// Usage record for billing
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct UsageRecord {
96    /// Tenant ID
97    pub tenant_id: String,
98    /// Operation type
99    pub operation: TenantOperation,
100    /// Number of operations
101    pub count: u64,
102    /// Timestamp
103    pub timestamp: DateTime<Utc>,
104    /// Cost (computed)
105    pub cost: f64,
106    /// Metadata
107    pub metadata: HashMap<String, String>,
108}
109
110impl UsageRecord {
111    /// Create new usage record
112    pub fn new(tenant_id: impl Into<String>, operation: TenantOperation, count: u64) -> Self {
113        Self {
114            tenant_id: tenant_id.into(),
115            operation,
116            count,
117            timestamp: Utc::now(),
118            cost: 0.0,
119            metadata: HashMap::new(),
120        }
121    }
122
123    /// Calculate cost using pricing model
124    pub fn calculate_cost(&mut self, pricing: &PricingModel) {
125        self.cost = pricing.calculate_cost(self.operation, self.count);
126    }
127}
128
129/// Billing metrics for a tenant
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct BillingMetrics {
132    /// Tenant ID
133    pub tenant_id: String,
134
135    /// Current billing period start
136    pub period_start: DateTime<Utc>,
137
138    /// Current billing period end
139    pub period_end: DateTime<Utc>,
140
141    /// Total cost for current period
142    pub total_cost: f64,
143
144    /// Total requests in period
145    pub total_requests: u64,
146
147    /// Average request cost
148    pub avg_request_cost: f64,
149
150    /// Cost by operation type
151    pub cost_by_operation: HashMap<String, f64>,
152
153    /// Request count by operation
154    pub requests_by_operation: HashMap<String, u64>,
155
156    /// Peak daily cost
157    pub peak_daily_cost: f64,
158
159    /// Estimated monthly cost (projected)
160    pub estimated_monthly_cost: f64,
161}
162
163impl BillingMetrics {
164    /// Create new billing metrics
165    pub fn new(tenant_id: impl Into<String>, period: BillingPeriod) -> Self {
166        let now = Utc::now();
167        let period_end = now + Duration::seconds(period.duration_secs());
168
169        Self {
170            tenant_id: tenant_id.into(),
171            period_start: now,
172            period_end,
173            total_cost: 0.0,
174            total_requests: 0,
175            avg_request_cost: 0.0,
176            cost_by_operation: HashMap::new(),
177            requests_by_operation: HashMap::new(),
178            peak_daily_cost: 0.0,
179            estimated_monthly_cost: 0.0,
180        }
181    }
182
183    /// Record usage
184    pub fn record_usage(&mut self, record: &UsageRecord) {
185        self.total_cost += record.cost;
186        self.total_requests += record.count;
187
188        let op_name = record.operation.name().to_string();
189        *self.cost_by_operation.entry(op_name.clone()).or_insert(0.0) += record.cost;
190        *self.requests_by_operation.entry(op_name).or_insert(0) += record.count;
191
192        // Update average
193        if self.total_requests > 0 {
194            self.avg_request_cost = self.total_cost / self.total_requests as f64;
195        }
196
197        // Update estimated monthly cost
198        let elapsed_secs = (Utc::now() - self.period_start).num_seconds() as f64;
199        if elapsed_secs > 0.0 {
200            let monthly_secs = 2592000.0; // 30 days
201            self.estimated_monthly_cost = self.total_cost * (monthly_secs / elapsed_secs);
202        }
203    }
204
205    /// Reset for new billing period
206    pub fn reset(&mut self, period: BillingPeriod) {
207        self.period_start = Utc::now();
208        self.period_end = self.period_start + Duration::seconds(period.duration_secs());
209        self.total_cost = 0.0;
210        self.total_requests = 0;
211        self.avg_request_cost = 0.0;
212        self.cost_by_operation.clear();
213        self.requests_by_operation.clear();
214    }
215}
216
217/// Billing engine for multi-tenancy
218pub struct BillingEngine {
219    /// Pricing models by tenant
220    pricing: Arc<Mutex<HashMap<String, PricingModel>>>,
221
222    /// Usage records
223    usage_history: Arc<Mutex<Vec<UsageRecord>>>,
224
225    /// Current billing metrics by tenant
226    metrics: Arc<Mutex<HashMap<String, BillingMetrics>>>,
227
228    /// Billing period
229    period: BillingPeriod,
230}
231
232impl BillingEngine {
233    /// Create new billing engine
234    pub fn new(period: BillingPeriod) -> Self {
235        Self {
236            pricing: Arc::new(Mutex::new(HashMap::new())),
237            usage_history: Arc::new(Mutex::new(Vec::new())),
238            metrics: Arc::new(Mutex::new(HashMap::new())),
239            period,
240        }
241    }
242
243    /// Set pricing model for tenant
244    pub fn set_pricing(
245        &self,
246        tenant_id: impl Into<String>,
247        pricing: PricingModel,
248    ) -> MultiTenancyResult<()> {
249        let tenant_id = tenant_id.into();
250
251        self.pricing
252            .lock()
253            .map_err(|e| MultiTenancyError::InternalError {
254                message: format!("Lock error: {}", e),
255            })?
256            .insert(tenant_id.clone(), pricing);
257
258        // Initialize metrics
259        self.metrics
260            .lock()
261            .map_err(|e| MultiTenancyError::InternalError {
262                message: format!("Lock error: {}", e),
263            })?
264            .entry(tenant_id.clone())
265            .or_insert_with(|| BillingMetrics::new(tenant_id, self.period));
266
267        Ok(())
268    }
269
270    /// Record usage for tenant
271    pub fn record_usage(
272        &self,
273        tenant_id: &str,
274        operation: TenantOperation,
275        count: u64,
276    ) -> MultiTenancyResult<f64> {
277        let mut record = UsageRecord::new(tenant_id, operation, count);
278
279        // Calculate cost
280        let pricing = self
281            .pricing
282            .lock()
283            .map_err(|e| MultiTenancyError::InternalError {
284                message: format!("Lock error: {}", e),
285            })?
286            .get(tenant_id)
287            .cloned()
288            .ok_or_else(|| MultiTenancyError::BillingError {
289                message: format!("No pricing model for tenant: {}", tenant_id),
290            })?;
291
292        record.calculate_cost(&pricing);
293        let cost = record.cost;
294
295        // Update metrics
296        let mut metrics = self
297            .metrics
298            .lock()
299            .map_err(|e| MultiTenancyError::InternalError {
300                message: format!("Lock error: {}", e),
301            })?;
302
303        metrics
304            .entry(tenant_id.to_string())
305            .or_insert_with(|| BillingMetrics::new(tenant_id, self.period))
306            .record_usage(&record);
307
308        // Store record
309        self.usage_history
310            .lock()
311            .map_err(|e| MultiTenancyError::InternalError {
312                message: format!("Lock error: {}", e),
313            })?
314            .push(record);
315
316        Ok(cost)
317    }
318
319    /// Get billing metrics for tenant
320    pub fn get_metrics(&self, tenant_id: &str) -> MultiTenancyResult<BillingMetrics> {
321        self.metrics
322            .lock()
323            .map_err(|e| MultiTenancyError::InternalError {
324                message: format!("Lock error: {}", e),
325            })?
326            .get(tenant_id)
327            .cloned()
328            .ok_or_else(|| MultiTenancyError::TenantNotFound {
329                tenant_id: tenant_id.to_string(),
330            })
331    }
332
333    /// Get usage history for tenant
334    pub fn get_usage_history(
335        &self,
336        tenant_id: &str,
337        start: DateTime<Utc>,
338        end: DateTime<Utc>,
339    ) -> MultiTenancyResult<Vec<UsageRecord>> {
340        let history = self
341            .usage_history
342            .lock()
343            .map_err(|e| MultiTenancyError::InternalError {
344                message: format!("Lock error: {}", e),
345            })?;
346
347        Ok(history
348            .iter()
349            .filter(|r| r.tenant_id == tenant_id && r.timestamp >= start && r.timestamp <= end)
350            .cloned()
351            .collect())
352    }
353
354    /// Reset billing period for tenant
355    pub fn reset_period(&self, tenant_id: &str) -> MultiTenancyResult<()> {
356        let mut metrics = self
357            .metrics
358            .lock()
359            .map_err(|e| MultiTenancyError::InternalError {
360                message: format!("Lock error: {}", e),
361            })?;
362
363        metrics
364            .get_mut(tenant_id)
365            .ok_or_else(|| MultiTenancyError::TenantNotFound {
366                tenant_id: tenant_id.to_string(),
367            })?
368            .reset(self.period);
369
370        Ok(())
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
377    use super::*;
378
379    #[test]
380    fn test_billing_period() {
381        assert_eq!(BillingPeriod::Hourly.duration_secs(), 3600);
382        assert_eq!(BillingPeriod::Daily.duration_secs(), 86400);
383        assert_eq!(BillingPeriod::Monthly.duration_secs(), 2592000);
384    }
385
386    #[test]
387    fn test_pricing_models() {
388        let model = PricingModel::PerRequest {
389            cost_per_request: 0.01,
390        };
391        assert_eq!(
392            model.calculate_cost(TenantOperation::VectorSearch, 100),
393            1.0
394        );
395
396        let model = PricingModel::PerComputeUnit { cost_per_unit: 0.1 };
397        let cost = model.calculate_cost(TenantOperation::IndexBuild, 1);
398        assert!(cost > 0.0); // Should be weighted by operation complexity
399    }
400
401    #[test]
402    fn test_usage_record() {
403        let mut record = UsageRecord::new("tenant1", TenantOperation::VectorSearch, 100);
404        assert_eq!(record.count, 100);
405        assert_eq!(record.cost, 0.0);
406
407        let pricing = PricingModel::PerRequest {
408            cost_per_request: 0.01,
409        };
410        record.calculate_cost(&pricing);
411        assert_eq!(record.cost, 1.0);
412    }
413
414    #[test]
415    fn test_billing_metrics() {
416        let mut metrics = BillingMetrics::new("tenant1", BillingPeriod::Daily);
417        assert_eq!(metrics.total_cost, 0.0);
418        assert_eq!(metrics.total_requests, 0);
419
420        let mut record = UsageRecord::new("tenant1", TenantOperation::VectorSearch, 100);
421        record.cost = 1.0;
422        metrics.record_usage(&record);
423
424        assert_eq!(metrics.total_cost, 1.0);
425        assert_eq!(metrics.total_requests, 100);
426        assert!((metrics.avg_request_cost - 0.01).abs() < 0.001);
427    }
428
429    #[test]
430    fn test_billing_engine() -> Result<()> {
431        let engine = BillingEngine::new(BillingPeriod::Daily);
432
433        // Set pricing
434        let pricing = PricingModel::PerRequest {
435            cost_per_request: 0.01,
436        };
437        engine.set_pricing("tenant1", pricing)?;
438
439        // Record usage
440        let cost = engine.record_usage("tenant1", TenantOperation::VectorSearch, 100)?;
441        assert_eq!(cost, 1.0);
442
443        // Get metrics
444        let metrics = engine.get_metrics("tenant1")?;
445        assert_eq!(metrics.total_cost, 1.0);
446        assert_eq!(metrics.total_requests, 100);
447
448        // Record more usage
449        engine.record_usage("tenant1", TenantOperation::VectorInsert, 50)?;
450
451        let metrics = engine.get_metrics("tenant1")?;
452        assert_eq!(metrics.total_cost, 1.5);
453        assert_eq!(metrics.total_requests, 150);
454        Ok(())
455    }
456
457    #[test]
458    fn test_usage_history() -> Result<()> {
459        let engine = BillingEngine::new(BillingPeriod::Daily);
460
461        let pricing = PricingModel::PerRequest {
462            cost_per_request: 0.01,
463        };
464        engine.set_pricing("tenant1", pricing)?;
465
466        // Record some usage
467        engine.record_usage("tenant1", TenantOperation::VectorSearch, 100)?;
468        engine.record_usage("tenant1", TenantOperation::VectorInsert, 50)?;
469
470        // Get history
471        let start = Utc::now() - Duration::hours(1);
472        let end = Utc::now() + Duration::hours(1);
473        let history = engine.get_usage_history("tenant1", start, end)?;
474
475        assert_eq!(history.len(), 2);
476        assert_eq!(history[0].count, 100);
477        assert_eq!(history[1].count, 50);
478        Ok(())
479    }
480
481    #[test]
482    fn test_subscription_pricing() {
483        let pricing = PricingModel::Subscription {
484            monthly_fee: 100.0,
485            included_requests: 10000,
486            overage_cost: 0.02,
487        };
488
489        // Subscription pricing is handled differently, just test structure
490        match pricing {
491            PricingModel::Subscription {
492                monthly_fee,
493                included_requests,
494                overage_cost,
495            } => {
496                assert_eq!(monthly_fee, 100.0);
497                assert_eq!(included_requests, 10000);
498                assert_eq!(overage_cost, 0.02);
499            }
500            _ => panic!("Expected subscription pricing"),
501        }
502    }
503}