1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8pub type TenantId = String;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum TenantStatus {
14 Active,
16
17 Suspended,
19
20 Trial,
22
23 Provisioning,
25
26 Decommissioning,
28
29 Deleted,
31}
32
33impl TenantStatus {
34 pub fn is_operational(&self) -> bool {
36 matches!(self, Self::Active | Self::Trial)
37 }
38
39 pub fn is_terminal(&self) -> bool {
41 matches!(self, Self::Deleted)
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TenantMetadata {
48 pub name: String,
50
51 pub organization: Option<String>,
53
54 pub email: Option<String>,
56
57 pub description: Option<String>,
59
60 pub billing_contact: Option<String>,
62
63 pub technical_contact: Option<String>,
65
66 pub region: Option<String>,
68
69 pub tags: HashMap<String, String>,
71
72 pub tier: String,
74}
75
76impl TenantMetadata {
77 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 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 pub fn get_tag(&self, key: &str) -> Option<&String> {
99 self.tags.get(key)
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct Tenant {
106 pub id: TenantId,
108
109 pub metadata: TenantMetadata,
111
112 pub status: TenantStatus,
114
115 pub created_at: DateTime<Utc>,
117
118 pub updated_at: DateTime<Utc>,
120
121 pub trial_expires_at: Option<DateTime<Utc>>,
123
124 pub namespace: String,
126
127 pub config: HashMap<String, String>,
129}
130
131impl Tenant {
132 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 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 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 fn generate_namespace(id: &str) -> String {
165 format!("tenant_{}", id.replace('-', "_"))
166 }
167
168 pub fn is_operational(&self) -> bool {
170 self.status.is_operational()
171 }
172
173 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 pub fn set_status(&mut self, status: TenantStatus) {
184 self.status = status;
185 self.updated_at = Utc::now();
186 }
187
188 pub fn suspend(&mut self) {
190 self.set_status(TenantStatus::Suspended);
191 }
192
193 pub fn activate(&mut self) {
195 self.set_status(TenantStatus::Active);
196 }
197
198 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 pub fn age_days(&self) -> i64 {
210 (Utc::now() - self.created_at).num_days()
211 }
212
213 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 pub fn get_config(&self, key: &str) -> Option<&String> {
221 self.config.get(key)
222 }
223
224 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}