use crate::ai::provider::ProviderSelection;
use crate::error::{ApiError, ApiResult};
use crate::handlers::usage::effective_limits;
use crate::models::{Organization, UsageCounter};
use crate::AppState;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct QuotaCheck {
pub limit: i64,
pub used: i64,
pub allowed: bool,
pub deny_reason: Option<String>,
}
impl QuotaCheck {
pub fn into_error(self) -> ApiError {
let reason = self.deny_reason.unwrap_or_else(|| "AI quota exceeded".to_string());
ApiError::ResourceLimitExceeded(reason)
}
}
pub async fn check_ai_quota(
state: &AppState,
org: &Organization,
selection: ProviderSelection,
) -> ApiResult<QuotaCheck> {
let limits = effective_limits(state, org).await?;
let limit = limits.get("ai_tokens_per_month").and_then(|v| v.as_i64()).unwrap_or(0);
let usage = state.store.get_or_create_current_usage_counter(org.id).await?;
let used = usage.ai_tokens_used;
match selection {
ProviderSelection::Disabled => Ok(QuotaCheck {
limit,
used,
allowed: false,
deny_reason: Some(
"AI features are not available on the Free plan without a BYOK provider key. \
Upgrade to Pro or add a BYOK key in Settings → BYOK."
.into(),
),
}),
ProviderSelection::Byok => {
Ok(QuotaCheck {
limit,
used,
allowed: true,
deny_reason: None,
})
}
ProviderSelection::Platform => {
if limit < 0 {
Ok(QuotaCheck {
limit,
used,
allowed: true,
deny_reason: None,
})
} else if used >= limit {
Ok(QuotaCheck {
limit,
used,
allowed: false,
deny_reason: Some(format!(
"Monthly AI token quota exhausted ({used} / {limit}). \
Add a BYOK provider key, upgrade your plan, or buy a top-up pack."
)),
})
} else {
Ok(QuotaCheck {
limit,
used,
allowed: true,
deny_reason: None,
})
}
}
}
}
pub async fn record_ai_usage(
state: &AppState,
org_id: Uuid,
selection: ProviderSelection,
tokens: i64,
) -> ApiResult<()> {
if tokens <= 0 || !matches!(selection, ProviderSelection::Platform) {
return Ok(());
}
UsageCounter::increment_ai_tokens(state.db.pool(), org_id, tokens)
.await
.map_err(ApiError::Database)?;
Ok(())
}