use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TierKind {
Free,
Pro,
Enterprise,
}
impl TierKind {
pub fn from_tier_str(s: &str) -> Self {
match s.to_ascii_lowercase().as_str() {
"pro" => TierKind::Pro,
"enterprise" => TierKind::Enterprise,
_ => TierKind::Free,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TierLimits {
pub max_documents: Option<u64>,
pub max_doc_bytes: Option<u64>,
pub max_api_calls_per_month: Option<u64>,
pub max_crdt_ops_per_month: Option<u64>,
pub max_agent_seats: Option<u64>,
pub max_storage_bytes: Option<u64>,
}
pub fn tier_limits(tier: TierKind) -> TierLimits {
match tier {
TierKind::Free => TierLimits {
max_documents: Some(50),
max_doc_bytes: Some(500 * 1024), max_api_calls_per_month: Some(1_000),
max_crdt_ops_per_month: Some(500),
max_agent_seats: Some(3),
max_storage_bytes: Some(25 * 1024 * 1024), },
TierKind::Pro => TierLimits {
max_documents: Some(500),
max_doc_bytes: Some(10 * 1024 * 1024), max_api_calls_per_month: Some(50_000),
max_crdt_ops_per_month: Some(25_000),
max_agent_seats: Some(25),
max_storage_bytes: Some(5 * 1024 * 1024 * 1024), },
TierKind::Enterprise => TierLimits {
max_documents: None,
max_doc_bytes: Some(100 * 1024 * 1024), max_api_calls_per_month: None,
max_crdt_ops_per_month: None,
max_agent_seats: None,
max_storage_bytes: None,
},
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageSnapshot {
pub document_count: u64,
pub api_calls_this_month: u64,
pub crdt_ops_this_month: u64,
pub agent_seat_count: u64,
pub storage_bytes: u64,
pub current_doc_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "lowercase")]
pub enum TierDecision {
Allowed,
Blocked {
limit_type: String,
current: u64,
limit: u64,
},
}
pub fn evaluate_tier_limits(usage: &UsageSnapshot, tier: TierKind) -> TierDecision {
let limits = tier_limits(tier);
if let Some(max) = limits.max_documents
&& usage.document_count >= max
{
return TierDecision::Blocked {
limit_type: "max_documents".into(),
current: usage.document_count,
limit: max,
};
}
if usage.current_doc_bytes > 0
&& let Some(max) = limits.max_doc_bytes
&& usage.current_doc_bytes > max
{
return TierDecision::Blocked {
limit_type: "max_doc_bytes".into(),
current: usage.current_doc_bytes,
limit: max,
};
}
if let Some(max) = limits.max_api_calls_per_month
&& usage.api_calls_this_month >= max
{
return TierDecision::Blocked {
limit_type: "max_api_calls_per_month".into(),
current: usage.api_calls_this_month,
limit: max,
};
}
if let Some(max) = limits.max_crdt_ops_per_month
&& usage.crdt_ops_this_month >= max
{
return TierDecision::Blocked {
limit_type: "max_crdt_ops_per_month".into(),
current: usage.crdt_ops_this_month,
limit: max,
};
}
if let Some(max) = limits.max_agent_seats
&& usage.agent_seat_count >= max
{
return TierDecision::Blocked {
limit_type: "max_agent_seats".into(),
current: usage.agent_seat_count,
limit: max,
};
}
if let Some(max) = limits.max_storage_bytes
&& usage.storage_bytes >= max
{
return TierDecision::Blocked {
limit_type: "max_storage_bytes".into(),
current: usage.storage_bytes,
limit: max,
};
}
TierDecision::Allowed
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn evaluate_tier_limits_wasm(usage_json: &str, tier_str: &str) -> String {
let usage: UsageSnapshot = match serde_json::from_str(usage_json) {
Ok(u) => u,
Err(e) => return format!(r#"{{"error":{}}}"#, serde_json::json!(e.to_string())),
};
let tier = TierKind::from_tier_str(tier_str);
let decision = evaluate_tier_limits(&usage, tier);
match serde_json::to_string(&decision) {
Ok(json) => json,
Err(e) => format!(r#"{{"error":{}}}"#, serde_json::json!(e.to_string())),
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn get_tier_limits_wasm(tier_str: &str) -> String {
let tier = TierKind::from_tier_str(tier_str);
let limits = tier_limits(tier);
match serde_json::to_string(&limits) {
Ok(json) => json,
Err(e) => format!(r#"{{"error":{}}}"#, serde_json::json!(e.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn free_usage_under_limit() -> UsageSnapshot {
UsageSnapshot {
document_count: 10,
api_calls_this_month: 100,
crdt_ops_this_month: 50,
agent_seat_count: 1,
storage_bytes: 1024 * 1024, current_doc_bytes: 0,
}
}
fn free_usage_at_doc_limit() -> UsageSnapshot {
UsageSnapshot {
document_count: 50,
..free_usage_under_limit()
}
}
fn free_usage_at_api_limit() -> UsageSnapshot {
UsageSnapshot {
api_calls_this_month: 1_000,
..free_usage_under_limit()
}
}
#[test]
fn test_free_tier_under_limit_allowed() {
let usage = free_usage_under_limit();
let decision = evaluate_tier_limits(&usage, TierKind::Free);
assert!(matches!(decision, TierDecision::Allowed));
}
#[test]
fn test_free_tier_doc_limit_blocked() {
let usage = free_usage_at_doc_limit();
let decision = evaluate_tier_limits(&usage, TierKind::Free);
match decision {
TierDecision::Blocked {
limit_type,
current,
limit,
} => {
assert_eq!(limit_type, "max_documents");
assert_eq!(current, 50);
assert_eq!(limit, 50);
}
_ => panic!("expected Blocked"),
}
}
#[test]
fn test_free_tier_api_limit_blocked() {
let usage = free_usage_at_api_limit();
let decision = evaluate_tier_limits(&usage, TierKind::Free);
match decision {
TierDecision::Blocked { limit_type, .. } => {
assert_eq!(limit_type, "max_api_calls_per_month");
}
_ => panic!("expected Blocked"),
}
}
#[test]
fn test_pro_tier_allows_more() {
let usage = UsageSnapshot {
document_count: 200,
api_calls_this_month: 30_000,
crdt_ops_this_month: 10_000,
agent_seat_count: 10,
storage_bytes: 1024 * 1024 * 1024, current_doc_bytes: 0,
};
let decision = evaluate_tier_limits(&usage, TierKind::Pro);
assert!(matches!(decision, TierDecision::Allowed));
}
#[test]
fn test_enterprise_unlimited_allows_heavy_usage() {
let usage = UsageSnapshot {
document_count: 1_000_000,
api_calls_this_month: 10_000_000,
crdt_ops_this_month: 5_000_000,
agent_seat_count: 10_000,
storage_bytes: u64::MAX / 2,
current_doc_bytes: 50 * 1024 * 1024, };
let decision = evaluate_tier_limits(&usage, TierKind::Enterprise);
assert!(matches!(decision, TierDecision::Allowed));
}
#[test]
fn test_enterprise_doc_byte_cap() {
let usage = UsageSnapshot {
current_doc_bytes: 200 * 1024 * 1024, ..free_usage_under_limit()
};
let decision = evaluate_tier_limits(&usage, TierKind::Enterprise);
match decision {
TierDecision::Blocked { limit_type, .. } => {
assert_eq!(limit_type, "max_doc_bytes");
}
_ => panic!("expected Blocked on enterprise doc byte cap"),
}
}
#[test]
fn test_tier_kind_parsing() {
assert_eq!(TierKind::from_tier_str("free"), TierKind::Free);
assert_eq!(TierKind::from_tier_str("pro"), TierKind::Pro);
assert_eq!(TierKind::from_tier_str("PRO"), TierKind::Pro);
assert_eq!(TierKind::from_tier_str("enterprise"), TierKind::Enterprise);
assert_eq!(TierKind::from_tier_str("unknown"), TierKind::Free);
assert_eq!(TierKind::from_tier_str(""), TierKind::Free);
}
#[test]
fn test_wasm_binding_allowed() {
let usage = serde_json::json!({
"document_count": 5,
"api_calls_this_month": 100,
"crdt_ops_this_month": 10,
"agent_seat_count": 1,
"storage_bytes": 1024,
"current_doc_bytes": 0
});
let result = evaluate_tier_limits_wasm(&usage.to_string(), "free");
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["status"], "allowed");
}
#[test]
fn test_wasm_binding_blocked() {
let usage = serde_json::json!({
"document_count": 50,
"api_calls_this_month": 100,
"crdt_ops_this_month": 10,
"agent_seat_count": 1,
"storage_bytes": 1024,
"current_doc_bytes": 0
});
let result = evaluate_tier_limits_wasm(&usage.to_string(), "free");
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["status"], "blocked");
assert_eq!(parsed["limit_type"], "max_documents");
}
#[test]
fn test_wasm_binding_invalid_json() {
let result = evaluate_tier_limits_wasm("not-json", "free");
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(parsed["error"].is_string());
}
#[test]
fn test_get_tier_limits_wasm() {
let result = get_tier_limits_wasm("pro");
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["max_documents"], 500);
assert_eq!(parsed["max_api_calls_per_month"], 50_000);
}
#[test]
fn test_deterministic_same_inputs_same_output() {
let usage = free_usage_at_doc_limit();
let d1 = evaluate_tier_limits(&usage, TierKind::Free);
let d2 = evaluate_tier_limits(&usage, TierKind::Free);
let j1 = serde_json::to_string(&d1).unwrap();
let j2 = serde_json::to_string(&d2).unwrap();
assert_eq!(j1, j2);
}
#[test]
fn test_doc_write_blocked_by_byte_size_free() {
let usage = UsageSnapshot {
document_count: 5,
api_calls_this_month: 100,
crdt_ops_this_month: 10,
agent_seat_count: 1,
storage_bytes: 1024,
current_doc_bytes: 600 * 1024, };
let decision = evaluate_tier_limits(&usage, TierKind::Free);
match decision {
TierDecision::Blocked { limit_type, .. } => {
assert_eq!(limit_type, "max_doc_bytes");
}
_ => panic!("expected Blocked"),
}
}
}