From ad61350c2e020fe3f703d015c952bd48784c65eb Mon Sep 17 00:00:00 2001 From: github-ops Date: Fri, 15 May 2026 10:16:47 +0000 Subject: [PATCH] core: add billing extension seam --- TASKS.md | 1 + crates/crank-core/src/ext/billing.rs | 75 ++++++++++++++++++++++++++++ crates/crank-core/src/ext/mod.rs | 1 + crates/crank-core/src/lib.rs | 3 ++ 4 files changed, 80 insertions(+) create mode 100644 crates/crank-core/src/ext/billing.rs diff --git a/TASKS.md b/TASKS.md index 28ac6cb..634c242 100644 --- a/TASKS.md +++ b/TASKS.md @@ -167,4 +167,5 @@ Progress: - Phase 2 dependency slice: `apps/admin-api` now exposes a reusable lib target (`src/lib.rs`), so downstream private repos can depend on `build_app`, `AppState`, `AdminServiceBuilder`, and auth/state modules without copying the Community admin app - Phase 3 dependency slice: added public tenancy seam in `crank-core` — `TenantId`, `TenantResolutionContext`, `TenantController`, `TenancyError`, and `SingleTenantController` — so `cloud` can build hosted tenant routing without bypassing the shared extension model - Phase 3 dependency slice: added public metering seam in `crank-core` — `MeteringEvent`, `MeteringSink`, `SharedMeteringSink`, and `NoopMeteringSink` — so `cloud` can add hosted usage capture without introducing private-only contracts + - Phase 3 dependency slice: added public billing seam in `crank-core` — `BillingHook`, `BillingGate`, `BillingError`, `SharedBillingHook`, and `NoopBillingHook` — so `cloud` can layer billing policy without private-only contracts in the base repo - backward-compatible alias `CommunityMachineCredentialVerifier` сохранен, поведение Community не изменено diff --git a/crates/crank-core/src/ext/billing.rs b/crates/crank-core/src/ext/billing.rs new file mode 100644 index 0000000..22233bc --- /dev/null +++ b/crates/crank-core/src/ext/billing.rs @@ -0,0 +1,75 @@ +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); + } +} diff --git a/crates/crank-core/src/ext/mod.rs b/crates/crank-core/src/ext/mod.rs index ad9c978..11952d1 100644 --- a/crates/crank-core/src/ext/mod.rs +++ b/crates/crank-core/src/ext/mod.rs @@ -1,6 +1,7 @@ pub mod access; pub mod audit; pub mod auth; +pub mod billing; pub mod capability; pub mod metering; pub mod protocol; diff --git a/crates/crank-core/src/lib.rs b/crates/crank-core/src/lib.rs index 99ab6c4..d488f39 100644 --- a/crates/crank-core/src/lib.rs +++ b/crates/crank-core/src/lib.rs @@ -47,6 +47,9 @@ pub use ext::auth::{ TokenIssuerActor, TokenIssuerError, TwoFactorActivation, TwoFactorChallenge, TwoFactorPendingIdentity, TwoFactorSetup, TwoFactorStatus, VerifiedMachineCredential, }; +pub use ext::billing::{ + BillingError, BillingGate, BillingHook, NoopBillingHook, SharedBillingHook, +}; pub use ext::capability::{CapabilityProfile, CommunityCapabilityProfile}; pub use ext::metering::{MeteringEvent, MeteringSink, NoopMeteringSink, SharedMeteringSink}; pub use ext::protocol::{