oxirs_vec/multi_tenancy/
tenant.rs

1//! Tenant representation and management
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8/// Unique identifier for a tenant
9pub type TenantId = String;
10
11/// Tenant status
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum TenantStatus {
14    /// Tenant is active and operational
15    Active,
16
17    /// Tenant is suspended (temporary)
18    Suspended,
19
20    /// Tenant is in trial period
21    Trial,
22
23    /// Tenant is being provisioned
24    Provisioning,
25
26    /// Tenant is being decommissioned
27    Decommissioning,
28
29    /// Tenant has been deleted
30    Deleted,
31}
32
33impl TenantStatus {
34    /// Check if tenant can perform operations
35    pub fn is_operational(&self) -> bool {
36        matches!(self, Self::Active | Self::Trial)
37    }
38
39    /// Check if tenant is in a terminal state
40    pub fn is_terminal(&self) -> bool {
41        matches!(self, Self::Deleted)
42    }
43}
44
45/// Tenant metadata
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TenantMetadata {
48    /// Tenant display name
49    pub name: String,
50
51    /// Organization/company name
52    pub organization: Option<String>,
53
54    /// Contact email
55    pub email: Option<String>,
56
57    /// Tenant description
58    pub description: Option<String>,
59
60    /// Billing contact
61    pub billing_contact: Option<String>,
62
63    /// Technical contact
64    pub technical_contact: Option<String>,
65
66    /// Region/datacenter location
67    pub region: Option<String>,
68
69    /// Custom tags for organization
70    pub tags: HashMap<String, String>,
71
72    /// Tenant tier (e.g., "free", "pro", "enterprise")
73    pub tier: String,
74}
75
76impl TenantMetadata {
77    /// Create new tenant metadata
78    pub fn new(name: impl Into<String>, tier: impl Into<String>) -> Self {
79        Self {
80            name: name.into(),
81            organization: None,
82            email: None,
83            description: None,
84            billing_contact: None,
85            technical_contact: None,
86            region: None,
87            tags: HashMap::new(),
88            tier: tier.into(),
89        }
90    }
91
92    /// Add a tag
93    pub fn add_tag(&mut self, key: impl Into<String>, value: impl Into<String>) {
94        self.tags.insert(key.into(), value.into());
95    }
96
97    /// Get a tag value
98    pub fn get_tag(&self, key: &str) -> Option<&String> {
99        self.tags.get(key)
100    }
101}
102
103/// Represents a tenant in the multi-tenant system
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Tenant {
106    /// Unique tenant identifier
107    pub id: TenantId,
108
109    /// Tenant metadata
110    pub metadata: TenantMetadata,
111
112    /// Current tenant status
113    pub status: TenantStatus,
114
115    /// Tenant creation timestamp
116    pub created_at: DateTime<Utc>,
117
118    /// Last updated timestamp
119    pub updated_at: DateTime<Utc>,
120
121    /// Trial expiration (if applicable)
122    pub trial_expires_at: Option<DateTime<Utc>>,
123
124    /// Namespace prefix for data isolation
125    pub namespace: String,
126
127    /// Custom configuration for the tenant
128    pub config: HashMap<String, String>,
129}
130
131impl Tenant {
132    /// Create a new tenant
133    pub fn new(id: impl Into<String>, metadata: TenantMetadata) -> Self {
134        let id = id.into();
135        let namespace = Self::generate_namespace(&id);
136
137        Self {
138            id,
139            metadata,
140            status: TenantStatus::Active,
141            created_at: Utc::now(),
142            updated_at: Utc::now(),
143            trial_expires_at: None,
144            namespace,
145            config: HashMap::new(),
146        }
147    }
148
149    /// Create a new tenant with auto-generated ID
150    pub fn new_with_auto_id(metadata: TenantMetadata) -> Self {
151        let id = Uuid::new_v4().to_string();
152        Self::new(id, metadata)
153    }
154
155    /// Create a tenant in trial mode
156    pub fn new_trial(id: impl Into<String>, metadata: TenantMetadata, trial_days: u32) -> Self {
157        let mut tenant = Self::new(id, metadata);
158        tenant.status = TenantStatus::Trial;
159        tenant.trial_expires_at = Some(Utc::now() + chrono::Duration::days(trial_days as i64));
160        tenant
161    }
162
163    /// Generate namespace from tenant ID
164    fn generate_namespace(id: &str) -> String {
165        format!("tenant_{}", id.replace('-', "_"))
166    }
167
168    /// Check if tenant is active and operational
169    pub fn is_operational(&self) -> bool {
170        self.status.is_operational()
171    }
172
173    /// Check if trial has expired
174    pub fn is_trial_expired(&self) -> bool {
175        if let Some(expires_at) = self.trial_expires_at {
176            Utc::now() > expires_at
177        } else {
178            false
179        }
180    }
181
182    /// Update tenant status
183    pub fn set_status(&mut self, status: TenantStatus) {
184        self.status = status;
185        self.updated_at = Utc::now();
186    }
187
188    /// Suspend the tenant
189    pub fn suspend(&mut self) {
190        self.set_status(TenantStatus::Suspended);
191    }
192
193    /// Activate the tenant
194    pub fn activate(&mut self) {
195        self.set_status(TenantStatus::Active);
196    }
197
198    /// Convert trial to paid
199    pub fn convert_trial_to_paid(&mut self, new_tier: impl Into<String>) {
200        if self.status == TenantStatus::Trial {
201            self.status = TenantStatus::Active;
202            self.trial_expires_at = None;
203            self.metadata.tier = new_tier.into();
204            self.updated_at = Utc::now();
205        }
206    }
207
208    /// Get tenant age in days
209    pub fn age_days(&self) -> i64 {
210        (Utc::now() - self.created_at).num_days()
211    }
212
213    /// Set configuration value
214    pub fn set_config(&mut self, key: impl Into<String>, value: impl Into<String>) {
215        self.config.insert(key.into(), value.into());
216        self.updated_at = Utc::now();
217    }
218
219    /// Get configuration value
220    pub fn get_config(&self, key: &str) -> Option<&String> {
221        self.config.get(key)
222    }
223
224    /// Get namespaced key for data isolation
225    pub fn namespaced_key(&self, key: &str) -> String {
226        format!("{}:{}", self.namespace, key)
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_tenant_creation() {
236        let metadata = TenantMetadata::new("Test Tenant", "pro");
237        let tenant = Tenant::new("tenant1", metadata);
238
239        assert_eq!(tenant.id, "tenant1");
240        assert_eq!(tenant.metadata.name, "Test Tenant");
241        assert_eq!(tenant.metadata.tier, "pro");
242        assert_eq!(tenant.status, TenantStatus::Active);
243        assert!(tenant.is_operational());
244    }
245
246    #[test]
247    fn test_tenant_auto_id() {
248        let metadata = TenantMetadata::new("Auto ID Tenant", "free");
249        let tenant = Tenant::new_with_auto_id(metadata);
250
251        assert!(!tenant.id.is_empty());
252        assert_eq!(tenant.metadata.name, "Auto ID Tenant");
253    }
254
255    #[test]
256    fn test_tenant_trial() {
257        let metadata = TenantMetadata::new("Trial Tenant", "trial");
258        let tenant = Tenant::new_trial("tenant2", metadata, 30);
259
260        assert_eq!(tenant.status, TenantStatus::Trial);
261        assert!(tenant.trial_expires_at.is_some());
262        assert!(!tenant.is_trial_expired());
263        assert!(tenant.is_operational());
264    }
265
266    #[test]
267    fn test_tenant_status_changes() {
268        let metadata = TenantMetadata::new("Test", "pro");
269        let mut tenant = Tenant::new("tenant3", metadata);
270
271        assert!(tenant.is_operational());
272
273        tenant.suspend();
274        assert_eq!(tenant.status, TenantStatus::Suspended);
275        assert!(!tenant.is_operational());
276
277        tenant.activate();
278        assert_eq!(tenant.status, TenantStatus::Active);
279        assert!(tenant.is_operational());
280    }
281
282    #[test]
283    fn test_trial_conversion() {
284        let metadata = TenantMetadata::new("Trial Convert", "trial");
285        let mut tenant = Tenant::new_trial("tenant4", metadata, 30);
286
287        assert_eq!(tenant.status, TenantStatus::Trial);
288        assert_eq!(tenant.metadata.tier, "trial");
289        assert!(tenant.trial_expires_at.is_some());
290
291        tenant.convert_trial_to_paid("enterprise");
292
293        assert_eq!(tenant.status, TenantStatus::Active);
294        assert_eq!(tenant.metadata.tier, "enterprise");
295        assert!(tenant.trial_expires_at.is_none());
296    }
297
298    #[test]
299    fn test_tenant_config() {
300        let metadata = TenantMetadata::new("Config Test", "pro");
301        let mut tenant = Tenant::new("tenant5", metadata);
302
303        tenant.set_config("max_vectors", "1000000");
304        tenant.set_config("index_type", "hnsw");
305
306        assert_eq!(
307            tenant.get_config("max_vectors"),
308            Some(&"1000000".to_string())
309        );
310        assert_eq!(tenant.get_config("index_type"), Some(&"hnsw".to_string()));
311        assert_eq!(tenant.get_config("nonexistent"), None);
312    }
313
314    #[test]
315    fn test_namespaced_key() {
316        let metadata = TenantMetadata::new("Namespace Test", "pro");
317        let tenant = Tenant::new("tenant6", metadata);
318
319        let key = tenant.namespaced_key("vectors");
320        assert!(key.contains("tenant_tenant6"));
321        assert!(key.contains("vectors"));
322    }
323
324    #[test]
325    fn test_tenant_metadata() {
326        let mut metadata = TenantMetadata::new("Metadata Test", "enterprise");
327        metadata.organization = Some("Acme Corp".to_string());
328        metadata.email = Some("admin@acme.com".to_string());
329        metadata.region = Some("us-west-2".to_string());
330        metadata.add_tag("environment", "production");
331        metadata.add_tag("cost_center", "engineering");
332
333        assert_eq!(metadata.organization, Some("Acme Corp".to_string()));
334        assert_eq!(
335            metadata.get_tag("environment"),
336            Some(&"production".to_string())
337        );
338        assert_eq!(
339            metadata.get_tag("cost_center"),
340            Some(&"engineering".to_string())
341        );
342        assert_eq!(metadata.get_tag("nonexistent"), None);
343    }
344
345    #[test]
346    fn test_tenant_status_operational() {
347        assert!(TenantStatus::Active.is_operational());
348        assert!(TenantStatus::Trial.is_operational());
349        assert!(!TenantStatus::Suspended.is_operational());
350        assert!(!TenantStatus::Deleted.is_operational());
351        assert!(!TenantStatus::Provisioning.is_operational());
352    }
353
354    #[test]
355    fn test_tenant_status_terminal() {
356        assert!(!TenantStatus::Active.is_terminal());
357        assert!(!TenantStatus::Suspended.is_terminal());
358        assert!(TenantStatus::Deleted.is_terminal());
359    }
360}