botserver/src/billing/quotas.rs

891 lines
30 KiB
Rust

use crate::billing::{BillingError, LimitValue, UsageMetric};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
pub struct QuotaManager {
usage_cache: Arc<RwLock<HashMap<Uuid, OrganizationQuotas>>>,
alert_thresholds: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct OrganizationQuotas {
pub organization_id: Uuid,
pub plan_id: String,
pub limits: QuotaLimits,
pub usage: QuotaUsage,
pub period_start: chrono::DateTime<chrono::Utc>,
pub period_end: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct QuotaLimits {
pub messages_per_day: LimitValue,
pub storage_bytes: LimitValue,
pub api_calls_per_day: LimitValue,
pub bots: LimitValue,
pub users: LimitValue,
pub kb_documents: LimitValue,
pub apps: LimitValue,
}
#[derive(Debug, Clone, Default)]
pub struct QuotaUsage {
pub messages_today: u64,
pub storage_bytes: u64,
pub api_calls_today: u64,
pub bots: u64,
pub users: u64,
pub kb_documents: u64,
pub apps: u64,
pub last_reset: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone)]
pub struct QuotaCheckResult {
pub allowed: bool,
pub metric: UsageMetric,
pub current: u64,
pub limit: Option<u64>,
pub remaining: Option<u64>,
pub percentage_used: f64,
pub alerts: Vec<QuotaAlert>,
}
#[derive(Debug, Clone)]
pub struct QuotaAlert {
pub metric: UsageMetric,
pub threshold: f64,
pub current_percentage: f64,
pub message: String,
}
#[derive(Debug, Clone)]
pub enum QuotaAction {
Allow,
Warn { message: String, percentage: f64 },
Block { message: String },
}
impl QuotaManager {
pub fn new() -> Self {
Self {
usage_cache: Arc::new(RwLock::new(HashMap::new())),
alert_thresholds: vec![80.0, 90.0, 100.0],
}
}
pub fn with_thresholds(thresholds: Vec<f64>) -> Self {
Self {
usage_cache: Arc::new(RwLock::new(HashMap::new())),
alert_thresholds: thresholds,
}
}
pub async fn set_quotas(&self, quotas: OrganizationQuotas) {
let mut cache = self.usage_cache.write().await;
cache.insert(quotas.organization_id, quotas);
}
pub async fn get_quotas(&self, organization_id: Uuid) -> Option<OrganizationQuotas> {
let cache = self.usage_cache.read().await;
cache.get(&organization_id).cloned()
}
pub async fn check_quota(
&self,
organization_id: Uuid,
metric: UsageMetric,
increment: u64,
) -> Result<QuotaCheckResult, BillingError> {
let cache = self.usage_cache.read().await;
let quotas = cache
.get(&organization_id)
.ok_or(BillingError::SubscriptionNotFound)?;
let (current, limit) = self.get_metric_values(quotas, metric);
let new_value = current + increment;
let (allowed, remaining, percentage) = match limit {
LimitValue::Unlimited => (true, None, 0.0),
LimitValue::Limited(max) => {
let allowed = new_value <= max;
let remaining = if new_value > max { 0 } else { max - new_value };
let percentage = (new_value as f64 / max as f64) * 100.0;
(allowed, Some(remaining), percentage)
}
};
let alerts = self.generate_alerts(metric, percentage);
Ok(QuotaCheckResult {
allowed,
metric,
current: new_value,
limit: limit.value(),
remaining,
percentage_used: percentage,
alerts,
})
}
pub async fn increment_usage(
&self,
organization_id: Uuid,
metric: UsageMetric,
amount: u64,
) -> Result<QuotaCheckResult, BillingError> {
let check_result = self.check_quota(organization_id, metric, amount).await?;
if check_result.allowed {
let mut cache = self.usage_cache.write().await;
if let Some(quotas) = cache.get_mut(&organization_id) {
self.apply_increment(&mut quotas.usage, metric, amount);
}
}
Ok(check_result)
}
pub async fn decrement_usage(
&self,
organization_id: Uuid,
metric: UsageMetric,
amount: u64,
) -> Result<(), BillingError> {
let mut cache = self.usage_cache.write().await;
let quotas = cache
.get_mut(&organization_id)
.ok_or(BillingError::SubscriptionNotFound)?;
self.apply_decrement(&mut quotas.usage, metric, amount);
Ok(())
}
pub async fn set_usage(
&self,
organization_id: Uuid,
metric: UsageMetric,
value: u64,
) -> Result<(), BillingError> {
let mut cache = self.usage_cache.write().await;
let quotas = cache
.get_mut(&organization_id)
.ok_or(BillingError::SubscriptionNotFound)?;
self.set_metric_value(&mut quotas.usage, metric, value);
Ok(())
}
pub async fn reset_daily_quotas(&self, organization_id: Uuid) -> Result<(), BillingError> {
let mut cache = self.usage_cache.write().await;
let quotas = cache
.get_mut(&organization_id)
.ok_or(BillingError::SubscriptionNotFound)?;
quotas.usage.messages_today = 0;
quotas.usage.api_calls_today = 0;
quotas.usage.last_reset = Some(chrono::Utc::now());
Ok(())
}
pub async fn reset_all_daily_quotas(&self) {
let mut cache = self.usage_cache.write().await;
let now = chrono::Utc::now();
for quotas in cache.values_mut() {
quotas.usage.messages_today = 0;
quotas.usage.api_calls_today = 0;
quotas.usage.last_reset = Some(now);
}
}
pub async fn get_usage_summary(&self, organization_id: Uuid) -> Result<UsageSummary, BillingError> {
let cache = self.usage_cache.read().await;
let quotas = cache
.get(&organization_id)
.ok_or(BillingError::SubscriptionNotFound)?;
let metrics = vec![
UsageMetric::Messages,
UsageMetric::StorageBytes,
UsageMetric::ApiCalls,
UsageMetric::Bots,
UsageMetric::Users,
UsageMetric::KbDocuments,
UsageMetric::Apps,
];
let items: Vec<UsageSummaryItem> = metrics
.into_iter()
.map(|metric| {
let (current, limit) = self.get_metric_values(quotas, metric);
let (remaining, percentage) = match limit {
LimitValue::Unlimited => (None, 0.0),
LimitValue::Limited(max) => {
let remaining = if current > max { 0 } else { max - current };
let percentage = (current as f64 / max as f64) * 100.0;
(Some(remaining), percentage)
}
};
UsageSummaryItem {
metric,
current,
limit: limit.value(),
remaining,
percentage_used: percentage,
is_unlimited: limit.is_unlimited(),
}
})
.collect();
Ok(UsageSummary {
organization_id,
plan_id: quotas.plan_id.clone(),
items,
period_start: quotas.period_start,
period_end: quotas.period_end,
})
}
pub async fn check_action(&self, organization_id: Uuid, metric: UsageMetric) -> QuotaAction {
match self.check_quota(organization_id, metric, 1).await {
Ok(result) => {
if !result.allowed {
QuotaAction::Block {
message: format!(
"Quota exceeded for {:?}. Current: {}, Limit: {:?}",
metric, result.current, result.limit
),
}
} else if result.percentage_used >= 90.0 {
QuotaAction::Warn {
message: format!(
"Approaching quota limit for {:?}. {}% used.",
metric, result.percentage_used as u32
),
percentage: result.percentage_used,
}
} else {
QuotaAction::Allow
}
}
Err(_) => QuotaAction::Block {
message: "Unable to verify quota".to_string(),
},
}
}
fn get_metric_values(&self, quotas: &OrganizationQuotas, metric: UsageMetric) -> (u64, LimitValue) {
match metric {
UsageMetric::Messages => (quotas.usage.messages_today, quotas.limits.messages_per_day),
UsageMetric::StorageBytes => (quotas.usage.storage_bytes, quotas.limits.storage_bytes),
UsageMetric::ApiCalls => (quotas.usage.api_calls_today, quotas.limits.api_calls_per_day),
UsageMetric::Bots => (quotas.usage.bots, quotas.limits.bots),
UsageMetric::Users => (quotas.usage.users, quotas.limits.users),
UsageMetric::KbDocuments => (quotas.usage.kb_documents, quotas.limits.kb_documents),
UsageMetric::Apps => (quotas.usage.apps, quotas.limits.apps),
}
}
fn apply_increment(&self, usage: &mut QuotaUsage, metric: UsageMetric, amount: u64) {
match metric {
UsageMetric::Messages => usage.messages_today += amount,
UsageMetric::StorageBytes => usage.storage_bytes += amount,
UsageMetric::ApiCalls => usage.api_calls_today += amount,
UsageMetric::Bots => usage.bots += amount,
UsageMetric::Users => usage.users += amount,
UsageMetric::KbDocuments => usage.kb_documents += amount,
UsageMetric::Apps => usage.apps += amount,
}
}
fn apply_decrement(&self, usage: &mut QuotaUsage, metric: UsageMetric, amount: u64) {
match metric {
UsageMetric::Messages => usage.messages_today = usage.messages_today.saturating_sub(amount),
UsageMetric::StorageBytes => usage.storage_bytes = usage.storage_bytes.saturating_sub(amount),
UsageMetric::ApiCalls => usage.api_calls_today = usage.api_calls_today.saturating_sub(amount),
UsageMetric::Bots => usage.bots = usage.bots.saturating_sub(amount),
UsageMetric::Users => usage.users = usage.users.saturating_sub(amount),
UsageMetric::KbDocuments => usage.kb_documents = usage.kb_documents.saturating_sub(amount),
UsageMetric::Apps => usage.apps = usage.apps.saturating_sub(amount),
}
}
fn set_metric_value(&self, usage: &mut QuotaUsage, metric: UsageMetric, value: u64) {
match metric {
UsageMetric::Messages => usage.messages_today = value,
UsageMetric::StorageBytes => usage.storage_bytes = value,
UsageMetric::ApiCalls => usage.api_calls_today = value,
UsageMetric::Bots => usage.bots = value,
UsageMetric::Users => usage.users = value,
UsageMetric::KbDocuments => usage.kb_documents = value,
UsageMetric::Apps => usage.apps = value,
}
}
fn generate_alerts(&self, metric: UsageMetric, percentage: f64) -> Vec<QuotaAlert> {
self.alert_thresholds
.iter()
.filter(|&&threshold| percentage >= threshold)
.map(|&threshold| QuotaAlert {
metric,
threshold,
current_percentage: percentage,
message: self.alert_message(metric, threshold, percentage),
})
.collect()
}
fn alert_message(&self, metric: UsageMetric, threshold: f64, current: f64) -> String {
let metric_name = match metric {
UsageMetric::Messages => "messages",
UsageMetric::StorageBytes => "storage",
UsageMetric::ApiCalls => "API calls",
UsageMetric::Bots => "bots",
UsageMetric::Users => "users",
UsageMetric::KbDocuments => "KB documents",
UsageMetric::Apps => "apps",
};
if current >= 100.0 {
format!("You have reached your {} quota limit.", metric_name)
} else {
format!(
"You have used {}% of your {} quota (threshold: {}%).",
current as u32, metric_name, threshold as u32
)
}
}
}
impl Default for QuotaManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct UsageSummary {
pub organization_id: Uuid,
pub plan_id: String,
pub items: Vec<UsageSummaryItem>,
pub period_start: chrono::DateTime<chrono::Utc>,
pub period_end: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone)]
pub struct UsageSummaryItem {
pub metric: UsageMetric,
pub current: u64,
pub limit: Option<u64>,
pub remaining: Option<u64>,
pub percentage_used: f64,
pub is_unlimited: bool,
}
pub struct QuotaMiddleware {
quota_manager: Arc<QuotaManager>,
}
impl QuotaMiddleware {
pub fn new(quota_manager: Arc<QuotaManager>) -> Self {
Self { quota_manager }
}
pub async fn check_and_increment(
&self,
organization_id: Uuid,
metric: UsageMetric,
) -> Result<QuotaCheckResult, BillingError> {
self.quota_manager.increment_usage(organization_id, metric, 1).await
}
pub async fn check_storage(
&self,
organization_id: Uuid,
bytes: u64,
) -> Result<QuotaCheckResult, BillingError> {
self.quota_manager.check_quota(organization_id, UsageMetric::StorageBytes, bytes).await
}
pub async fn add_storage(
&self,
organization_id: Uuid,
bytes: u64,
) -> Result<QuotaCheckResult, BillingError> {
self.quota_manager.increment_usage(organization_id, UsageMetric::StorageBytes, bytes).await
}
pub async fn remove_storage(
&self,
organization_id: Uuid,
bytes: u64,
) -> Result<(), BillingError> {
self.quota_manager.decrement_usage(organization_id, UsageMetric::StorageBytes, bytes).await
}
}
pub async fn daily_quota_reset_job(quota_manager: Arc<QuotaManager>) {
loop {
let now = chrono::Utc::now();
let tomorrow = (now + chrono::Duration::days(1))
.date_naive()
.and_hms_opt(0, 0, 0)
.map(|dt| chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(dt, chrono::Utc));
if let Some(next_reset) = tomorrow {
let duration = next_reset - now;
if let Ok(std_duration) = duration.to_std() {
tokio::time::sleep(std_duration).await;
}
}
quota_manager.reset_all_daily_quotas().await;
tracing::info!("Daily quotas reset completed");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::billing::LimitValue;
fn create_test_quotas(org_id: Uuid, plan_id: &str) -> OrganizationQuotas {
let now = chrono::Utc::now();
OrganizationQuotas {
organization_id: org_id,
plan_id: plan_id.to_string(),
limits: QuotaLimits {
messages_per_day: LimitValue::Limited(100),
storage_bytes: LimitValue::Limited(1024 * 1024 * 100),
api_calls_per_day: LimitValue::Limited(1000),
bots: LimitValue::Limited(5),
users: LimitValue::Limited(10),
kb_documents: LimitValue::Limited(50),
apps: LimitValue::Limited(10),
},
usage: QuotaUsage::default(),
period_start: now,
period_end: now + chrono::Duration::days(30),
}
}
fn create_unlimited_quotas(org_id: Uuid) -> OrganizationQuotas {
let now = chrono::Utc::now();
OrganizationQuotas {
organization_id: org_id,
plan_id: "enterprise".to_string(),
limits: QuotaLimits {
messages_per_day: LimitValue::Unlimited,
storage_bytes: LimitValue::Unlimited,
api_calls_per_day: LimitValue::Unlimited,
bots: LimitValue::Unlimited,
users: LimitValue::Unlimited,
kb_documents: LimitValue::Unlimited,
apps: LimitValue::Unlimited,
},
usage: QuotaUsage::default(),
period_start: now,
period_end: now + chrono::Duration::days(30),
}
}
#[tokio::test]
async fn test_set_and_get_quotas() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas.clone()).await;
let retrieved = manager.get_quotas(org_id).await;
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.organization_id, org_id);
assert_eq!(retrieved.plan_id, "business");
}
#[tokio::test]
async fn test_get_quotas_nonexistent() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let result = manager.get_quotas(org_id).await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_check_quota_within_limit() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager
.check_quota(org_id, UsageMetric::Messages, 50)
.await;
assert!(result.is_ok());
let check = result.unwrap();
assert!(check.allowed);
assert_eq!(check.current, 50);
assert_eq!(check.limit, Some(100));
assert_eq!(check.remaining, Some(50));
assert_eq!(check.percentage_used, 50.0);
}
#[tokio::test]
async fn test_check_quota_at_limit() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager
.check_quota(org_id, UsageMetric::Messages, 100)
.await;
assert!(result.is_ok());
let check = result.unwrap();
assert!(check.allowed);
assert_eq!(check.remaining, Some(0));
assert_eq!(check.percentage_used, 100.0);
}
#[tokio::test]
async fn test_check_quota_exceeds_limit() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager
.check_quota(org_id, UsageMetric::Messages, 101)
.await;
assert!(result.is_ok());
let check = result.unwrap();
assert!(!check.allowed);
assert_eq!(check.remaining, Some(0));
}
#[tokio::test]
async fn test_check_quota_unlimited() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_unlimited_quotas(org_id);
manager.set_quotas(quotas).await;
let result = manager
.check_quota(org_id, UsageMetric::Messages, 1_000_000)
.await;
assert!(result.is_ok());
let check = result.unwrap();
assert!(check.allowed);
assert_eq!(check.limit, None);
assert_eq!(check.remaining, None);
assert_eq!(check.percentage_used, 0.0);
}
#[tokio::test]
async fn test_check_quota_subscription_not_found() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let result = manager
.check_quota(org_id, UsageMetric::Messages, 1)
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), BillingError::SubscriptionNotFound));
}
#[tokio::test]
async fn test_increment_usage() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager
.increment_usage(org_id, UsageMetric::Messages, 10)
.await;
assert!(result.is_ok());
assert!(result.unwrap().allowed);
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.messages_today, 10);
}
#[tokio::test]
async fn test_increment_usage_blocked_when_exceeded() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager
.increment_usage(org_id, UsageMetric::Messages, 150)
.await;
assert!(result.is_ok());
assert!(!result.unwrap().allowed);
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.messages_today, 0);
}
#[tokio::test]
async fn test_decrement_usage() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
manager.increment_usage(org_id, UsageMetric::Bots, 3).await.unwrap();
let result = manager.decrement_usage(org_id, UsageMetric::Bots, 1).await;
assert!(result.is_ok());
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.bots, 2);
}
#[tokio::test]
async fn test_decrement_usage_saturating() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager.decrement_usage(org_id, UsageMetric::Bots, 100).await;
assert!(result.is_ok());
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.bots, 0);
}
#[tokio::test]
async fn test_set_usage() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager.set_usage(org_id, UsageMetric::StorageBytes, 5000).await;
assert!(result.is_ok());
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.storage_bytes, 5000);
}
#[tokio::test]
async fn test_reset_daily_quotas() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
manager.increment_usage(org_id, UsageMetric::Messages, 50).await.unwrap();
manager.increment_usage(org_id, UsageMetric::ApiCalls, 100).await.unwrap();
manager.increment_usage(org_id, UsageMetric::Bots, 2).await.unwrap();
let result = manager.reset_daily_quotas(org_id).await;
assert!(result.is_ok());
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.messages_today, 0);
assert_eq!(quotas.usage.api_calls_today, 0);
assert_eq!(quotas.usage.bots, 2);
assert!(quotas.usage.last_reset.is_some());
}
#[tokio::test]
async fn test_reset_all_daily_quotas() {
let manager = QuotaManager::new();
let org_id1 = Uuid::new_v4();
let org_id2 = Uuid::new_v4();
manager.set_quotas(create_test_quotas(org_id1, "business")).await;
manager.set_quotas(create_test_quotas(org_id2, "personal")).await;
manager.increment_usage(org_id1, UsageMetric::Messages, 50).await.unwrap();
manager.increment_usage(org_id2, UsageMetric::Messages, 30).await.unwrap();
manager.reset_all_daily_quotas().await;
let q1 = manager.get_quotas(org_id1).await.unwrap();
let q2 = manager.get_quotas(org_id2).await.unwrap();
assert_eq!(q1.usage.messages_today, 0);
assert_eq!(q2.usage.messages_today, 0);
}
#[tokio::test]
async fn test_get_usage_summary() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
manager.increment_usage(org_id, UsageMetric::Messages, 50).await.unwrap();
manager.increment_usage(org_id, UsageMetric::Bots, 2).await.unwrap();
let result = manager.get_usage_summary(org_id).await;
assert!(result.is_ok());
let summary = result.unwrap();
assert_eq!(summary.organization_id, org_id);
assert_eq!(summary.plan_id, "business");
assert_eq!(summary.items.len(), 7);
let messages_item = summary.items.iter().find(|i| i.metric == UsageMetric::Messages).unwrap();
assert_eq!(messages_item.current, 50);
assert_eq!(messages_item.limit, Some(100));
assert_eq!(messages_item.remaining, Some(50));
assert_eq!(messages_item.percentage_used, 50.0);
assert!(!messages_item.is_unlimited);
}
#[tokio::test]
async fn test_check_action_allow() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let action = manager.check_action(org_id, UsageMetric::Messages).await;
assert!(matches!(action, QuotaAction::Allow));
}
#[tokio::test]
async fn test_check_action_warn() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
manager.set_usage(org_id, UsageMetric::Messages, 91).await.unwrap();
let action = manager.check_action(org_id, UsageMetric::Messages).await;
assert!(matches!(action, QuotaAction::Warn { .. }));
}
#[tokio::test]
async fn test_check_action_block() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
manager.set_usage(org_id, UsageMetric::Messages, 100).await.unwrap();
let action = manager.check_action(org_id, UsageMetric::Messages).await;
assert!(matches!(action, QuotaAction::Block { .. }));
}
#[tokio::test]
async fn test_alerts_generated_at_thresholds() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let result = manager.check_quota(org_id, UsageMetric::Messages, 85).await.unwrap();
assert_eq!(result.alerts.len(), 1);
assert_eq!(result.alerts[0].threshold, 80.0);
let result = manager.check_quota(org_id, UsageMetric::Messages, 95).await.unwrap();
assert_eq!(result.alerts.len(), 2);
let result = manager.check_quota(org_id, UsageMetric::Messages, 100).await.unwrap();
assert_eq!(result.alerts.len(), 3);
}
#[tokio::test]
async fn test_quota_middleware_check_and_increment() {
let manager = Arc::new(QuotaManager::new());
let middleware = QuotaMiddleware::new(manager.clone());
let org_id = Uuid::new_v4();
manager.set_quotas(create_test_quotas(org_id, "business")).await;
let result = middleware.check_and_increment(org_id, UsageMetric::ApiCalls).await;
assert!(result.is_ok());
assert!(result.unwrap().allowed);
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.api_calls_today, 1);
}
#[tokio::test]
async fn test_quota_middleware_storage_operations() {
let manager = Arc::new(QuotaManager::new());
let middleware = QuotaMiddleware::new(manager.clone());
let org_id = Uuid::new_v4();
manager.set_quotas(create_test_quotas(org_id, "business")).await;
let check = middleware.check_storage(org_id, 1000).await;
assert!(check.is_ok());
assert!(check.unwrap().allowed);
let add = middleware.add_storage(org_id, 1000).await;
assert!(add.is_ok());
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.storage_bytes, 1000);
let remove = middleware.remove_storage(org_id, 500).await;
assert!(remove.is_ok());
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.storage_bytes, 500);
}
#[test]
fn test_quota_usage_default() {
let usage = QuotaUsage::default();
assert_eq!(usage.messages_today, 0);
assert_eq!(usage.storage_bytes, 0);
assert_eq!(usage.api_calls_today, 0);
assert_eq!(usage.bots, 0);
assert_eq!(usage.users, 0);
assert_eq!(usage.kb_documents, 0);
assert_eq!(usage.apps, 0);
assert!(usage.last_reset.is_none());
}
#[tokio::test]
async fn test_all_metric_types() {
let manager = QuotaManager::new();
let org_id = Uuid::new_v4();
let quotas = create_test_quotas(org_id, "business");
manager.set_quotas(quotas).await;
let metrics = vec![
(UsageMetric::Messages, 10),
(UsageMetric::StorageBytes, 1000),
(UsageMetric::ApiCalls, 5),
(UsageMetric::Bots, 1),
(UsageMetric::Users, 2),
(UsageMetric::KbDocuments, 3),
(UsageMetric::Apps, 1),
];
for (metric, amount) in metrics {
let result = manager.increment_usage(org_id, metric, amount).await;
assert!(result.is_ok(), "Failed for metric {:?}", metric);
assert!(result.unwrap().allowed, "Not allowed for metric {:?}", metric);
}
let quotas = manager.get_quotas(org_id).await.unwrap();
assert_eq!(quotas.usage.messages_today, 10);
assert_eq!(quotas.usage.storage_bytes, 1000);
assert_eq!(quotas.usage.api_calls_today, 5);
assert_eq!(quotas.usage.bots, 1);
assert_eq!(quotas.usage.users, 2);
assert_eq!(quotas.usage.kb_documents, 3);
assert_eq!(quotas.usage.apps, 1);
}
}