use super::{Metadata, UsageStats, UserRateLimits};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
#[serde(flatten)]
pub metadata: Metadata,
pub name: String,
pub display_name: Option<String>,
pub description: Option<String>,
pub status: TeamStatus,
pub settings: TeamSettings,
pub usage_stats: UsageStats,
pub rate_limits: Option<UserRateLimits>,
pub billing: Option<TeamBilling>,
pub team_metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TeamStatus {
Active,
Inactive,
Suspended,
Deleted,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TeamSettings {
pub default_member_role: Option<String>,
pub require_approval: bool,
pub allow_member_invites: bool,
pub visibility: TeamVisibility,
pub api_access: ApiAccessSettings,
pub notifications: TeamNotificationSettings,
pub security: TeamSecuritySettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TeamVisibility {
Public,
Private,
Internal,
}
impl Default for TeamVisibility {
fn default() -> Self {
Self::Private
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ApiAccessSettings {
pub enabled: bool,
pub allowed_ips: Vec<String>,
pub allowed_domains: Vec<String>,
pub require_api_key: bool,
pub default_settings: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TeamNotificationSettings {
pub slack_webhook: Option<String>,
pub email_notifications: bool,
pub webhook_notifications: bool,
pub channels: Vec<NotificationChannel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationChannel {
pub name: String,
pub channel_type: ChannelType,
pub config: HashMap<String, serde_json::Value>,
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelType {
Email,
Slack,
Webhook,
Teams,
Discord,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TeamSecuritySettings {
pub require_2fa: bool,
pub password_policy: PasswordPolicy,
pub session_timeout: Option<u32>,
pub ip_whitelist: Vec<String>,
pub audit_logging: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PasswordPolicy {
pub min_length: u32,
pub require_uppercase: bool,
pub require_lowercase: bool,
pub require_numbers: bool,
pub require_special: bool,
pub expiry_days: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamBilling {
pub plan: BillingPlan,
pub status: BillingStatus,
pub monthly_budget: Option<f64>,
pub current_usage: f64,
pub cycle_start: chrono::DateTime<chrono::Utc>,
pub cycle_end: chrono::DateTime<chrono::Utc>,
pub payment_method: Option<PaymentMethod>,
pub billing_address: Option<BillingAddress>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BillingPlan {
Free,
Starter,
Professional,
Enterprise,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BillingStatus {
Active,
PastDue,
Cancelled,
Suspended,
Trial,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentMethod {
pub method_type: PaymentMethodType,
pub last_four: Option<String>,
pub expiry_month: Option<u32>,
pub expiry_year: Option<u32>,
pub brand: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PaymentMethodType {
CreditCard,
DebitCard,
BankTransfer,
PayPal,
Stripe,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BillingAddress {
pub company: Option<String>,
pub line1: String,
pub line2: Option<String>,
pub city: String,
pub state: Option<String>,
pub postal_code: String,
pub country: String,
pub tax_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamMember {
#[serde(flatten)]
pub metadata: Metadata,
pub team_id: Uuid,
pub user_id: Uuid,
pub role: TeamRole,
pub status: MemberStatus,
pub joined_at: chrono::DateTime<chrono::Utc>,
pub invited_by: Option<Uuid>,
pub permissions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TeamRole {
Owner,
Admin,
Manager,
Member,
Viewer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemberStatus {
Active,
Pending,
Suspended,
Left,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamInvitation {
#[serde(flatten)]
pub metadata: Metadata,
pub team_id: Uuid,
pub email: String,
pub role: TeamRole,
#[serde(skip_serializing)]
pub token: String,
pub invited_by: Uuid,
pub expires_at: chrono::DateTime<chrono::Utc>,
pub status: InvitationStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvitationStatus {
Pending,
Accepted,
Declined,
Expired,
Cancelled,
}
impl Team {
pub fn new(name: String, display_name: Option<String>) -> Self {
Self {
metadata: Metadata::new(),
name,
display_name,
description: None,
status: TeamStatus::Active,
settings: TeamSettings::default(),
usage_stats: UsageStats::default(),
rate_limits: None,
billing: None,
team_metadata: HashMap::new(),
}
}
pub fn id(&self) -> Uuid {
self.metadata.id
}
pub fn is_active(&self) -> bool {
matches!(self.status, TeamStatus::Active)
}
pub fn update_usage(&mut self, requests: u64, tokens: u64, cost: f64) {
self.usage_stats.total_requests += requests;
self.usage_stats.total_tokens += tokens;
self.usage_stats.total_cost += cost;
let today = chrono::Utc::now().date_naive();
let last_reset = self.usage_stats.last_reset.date_naive();
if today != last_reset {
self.usage_stats.requests_today = 0;
self.usage_stats.tokens_today = 0;
self.usage_stats.cost_today = 0.0;
self.usage_stats.last_reset = chrono::Utc::now();
}
self.usage_stats.requests_today += requests as u32;
self.usage_stats.tokens_today += tokens as u32;
self.usage_stats.cost_today += cost;
if let Some(billing) = &mut self.billing {
billing.current_usage += cost;
}
self.metadata.touch();
}
pub fn is_over_budget(&self) -> bool {
if let Some(billing) = &self.billing {
if let Some(budget) = billing.monthly_budget {
return billing.current_usage >= budget;
}
}
false
}
pub fn remaining_budget(&self) -> Option<f64> {
if let Some(billing) = &self.billing {
if let Some(budget) = billing.monthly_budget {
return Some((budget - billing.current_usage).max(0.0));
}
}
None
}
}
impl TeamMember {
pub fn new(team_id: Uuid, user_id: Uuid, role: TeamRole, invited_by: Option<Uuid>) -> Self {
Self {
metadata: Metadata::new(),
team_id,
user_id,
role,
status: MemberStatus::Active,
joined_at: chrono::Utc::now(),
invited_by,
permissions: vec![],
}
}
pub fn is_active(&self) -> bool {
matches!(self.status, MemberStatus::Active)
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions.contains(&permission.to_string())
}
pub fn add_permission(&mut self, permission: String) {
if !self.permissions.contains(&permission) {
self.permissions.push(permission);
self.metadata.touch();
}
}
pub fn remove_permission(&mut self, permission: &str) {
if let Some(pos) = self.permissions.iter().position(|p| p == permission) {
self.permissions.remove(pos);
self.metadata.touch();
}
}
}
impl TeamInvitation {
pub fn new(
team_id: Uuid,
email: String,
role: TeamRole,
token: String,
invited_by: Uuid,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Self {
Self {
metadata: Metadata::new(),
team_id,
email,
role,
token,
invited_by,
expires_at,
status: InvitationStatus::Pending,
}
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now() > self.expires_at
}
pub fn accept(&mut self) {
self.status = InvitationStatus::Accepted;
self.metadata.touch();
}
pub fn decline(&mut self) {
self.status = InvitationStatus::Declined;
self.metadata.touch();
}
pub fn cancel(&mut self) {
self.status = InvitationStatus::Cancelled;
self.metadata.touch();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_team_creation() {
let team = Team::new("test-team".to_string(), Some("Test Team".to_string()));
assert_eq!(team.name, "test-team");
assert_eq!(team.display_name, Some("Test Team".to_string()));
assert!(team.is_active());
}
#[test]
fn test_team_usage_update() {
let mut team = Team::new("test-team".to_string(), None);
team.update_usage(10, 1000, 0.50);
assert_eq!(team.usage_stats.total_requests, 10);
assert_eq!(team.usage_stats.total_tokens, 1000);
assert_eq!(team.usage_stats.total_cost, 0.50);
}
#[test]
fn test_team_member_permissions() {
let team_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let mut member = TeamMember::new(team_id, user_id, TeamRole::Member, None);
assert!(!member.has_permission("admin"));
member.add_permission("admin".to_string());
assert!(member.has_permission("admin"));
member.remove_permission("admin");
assert!(!member.has_permission("admin"));
}
#[test]
fn test_invitation_expiry() {
let team_id = Uuid::new_v4();
let invited_by = Uuid::new_v4();
let expires_at = chrono::Utc::now() - chrono::Duration::hours(1);
let invitation = TeamInvitation::new(
team_id,
"test@example.com".to_string(),
TeamRole::Member,
"token".to_string(),
invited_by,
expires_at,
);
assert!(invitation.is_expired());
}
}