use std::sync::Arc; use async_trait::async_trait; use crate::{TenantId, UsageRollup}; #[derive(Clone, Debug, PartialEq, Eq)] pub enum BillingGate { Allow, Deny { reason: String }, } #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum BillingError { #[error("billing integration is not available in this edition")] NotSupportedInEdition, #[error("billing provider failure: {0}")] Provider(String), } #[async_trait] pub trait BillingHook: Send + Sync { async fn on_rollup(&self, rollup: UsageRollup) -> Result<(), BillingError>; async fn check_billing_gate(&self, tenant: &TenantId) -> Result; } pub type SharedBillingHook = Arc; #[derive(Clone, Copy, Debug, Default)] pub struct NoopBillingHook; #[async_trait] impl BillingHook for NoopBillingHook { async fn on_rollup(&self, _rollup: UsageRollup) -> Result<(), BillingError> { Ok(()) } async fn check_billing_gate(&self, _tenant: &TenantId) -> Result { Ok(BillingGate::Allow) } } #[cfg(test)] mod tests { use super::{BillingGate, BillingHook, NoopBillingHook}; use crate::{TenantId, UsagePeriod, UsageRollup, WorkspaceId}; #[tokio::test] async fn noop_billing_hook_allows_default_gate_and_rollups() { let hook = NoopBillingHook; hook.on_rollup(UsageRollup { workspace_id: WorkspaceId::new("ws_01"), agent_id: None, operation_id: None, period: UsagePeriod::Last24Hours, calls_total: 10, calls_ok: 9, calls_error: 1, p50_ms: 12, p95_ms: 42, p99_ms: 77, }) .await .unwrap(); let gate = hook .check_billing_gate(&TenantId::new("tenant_01")) .await .unwrap(); assert_eq!(gate, BillingGate::Allow); } }