use super::repository::TeamRepository;
use crate::core::models::team::{Team, TeamMember, TeamRole, TeamSettings, TeamStatus};
use crate::utils::error::gateway_error::{GatewayError, Result};
use std::sync::Arc;
use tracing::{debug, info};
use uuid::Uuid;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TeamUsageStats {
pub team_id: Uuid,
pub team_name: String,
pub total_requests: u64,
pub total_tokens: u64,
pub total_cost: f64,
pub requests_today: u32,
pub tokens_today: u32,
pub cost_today: f64,
pub member_count: usize,
pub budget_usage_percent: Option<f64>,
pub remaining_budget: Option<f64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CreateTeamRequest {
pub name: String,
pub display_name: Option<String>,
pub description: Option<String>,
pub settings: Option<TeamSettings>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UpdateTeamRequest {
pub name: Option<String>,
pub display_name: Option<String>,
pub description: Option<String>,
pub settings: Option<TeamSettings>,
pub status: Option<TeamStatus>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AddMemberRequest {
pub user_id: Uuid,
pub role: TeamRole,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UpdateRoleRequest {
pub role: TeamRole,
}
pub struct TeamManager {
repository: Arc<dyn TeamRepository>,
}
impl TeamManager {
pub fn new(repository: Arc<dyn TeamRepository>) -> Self {
Self { repository }
}
pub async fn create_team(&self, request: CreateTeamRequest) -> Result<Team> {
info!("Creating team: {}", request.name);
self.validate_team_name(&request.name)?;
if self.repository.get_by_name(&request.name).await?.is_some() {
return Err(GatewayError::Conflict(format!(
"Team with name '{}' already exists",
request.name
)));
}
let mut team = Team::new(request.name.clone(), request.display_name);
team.description = request.description;
if let Some(settings) = request.settings {
team.settings = settings;
}
let created = self.repository.create(team).await?;
info!("Team created: {} ({})", created.name, created.id());
Ok(created)
}
pub async fn get_team(&self, id: Uuid) -> Result<Team> {
debug!("Fetching team: {}", id);
self.repository
.get(id)
.await?
.ok_or_else(|| GatewayError::NotFound(format!("Team {} not found", id)))
}
pub async fn get_team_by_name(&self, name: &str) -> Result<Team> {
debug!("Fetching team by name: {}", name);
self.repository
.get_by_name(name)
.await?
.ok_or_else(|| GatewayError::NotFound(format!("Team '{}' not found", name)))
}
pub async fn update_team(&self, id: Uuid, request: UpdateTeamRequest) -> Result<Team> {
info!("Updating team: {}", id);
let mut team = self.get_team(id).await?;
if let Some(name) = request.name {
self.validate_team_name(&name)?;
if let Some(existing) = self.repository.get_by_name(&name).await?
&& existing.id() != id
{
return Err(GatewayError::Conflict(format!(
"Team with name '{}' already exists",
name
)));
}
team.name = name;
}
if let Some(display_name) = request.display_name {
team.display_name = Some(display_name);
}
if let Some(description) = request.description {
team.description = Some(description);
}
if let Some(settings) = request.settings {
team.settings = settings;
}
if let Some(status) = request.status {
team.status = status;
}
let updated = self.repository.update(team).await?;
info!("Team updated: {} ({})", updated.name, updated.id());
Ok(updated)
}
pub async fn delete_team(&self, id: Uuid) -> Result<()> {
info!("Deleting team: {}", id);
let team = self.get_team(id).await?;
self.repository.delete(id).await?;
info!("Team deleted: {} ({})", team.name, id);
Ok(())
}
pub async fn list_teams(&self, offset: u32, limit: u32) -> Result<(Vec<Team>, u64)> {
debug!("Listing teams: offset={}, limit={}", offset, limit);
self.repository.list(offset, limit).await
}
pub async fn add_member(
&self,
team_id: Uuid,
request: AddMemberRequest,
invited_by: Option<Uuid>,
) -> Result<TeamMember> {
info!(
"Adding member {} to team {} with role {:?}",
request.user_id, team_id, request.role
);
let _team = self.get_team(team_id).await?;
if self
.repository
.get_member(team_id, request.user_id)
.await?
.is_some()
{
return Err(GatewayError::Conflict(format!(
"User {} is already a member of team {}",
request.user_id, team_id
)));
}
let member = TeamMember::new(team_id, request.user_id, request.role, invited_by);
let created = self.repository.add_member(member).await?;
info!(
"Member {} added to team {} with role {:?}",
created.user_id, team_id, created.role
);
Ok(created)
}
pub async fn get_member(&self, team_id: Uuid, user_id: Uuid) -> Result<TeamMember> {
debug!("Fetching member {} from team {}", user_id, team_id);
self.repository
.get_member(team_id, user_id)
.await?
.ok_or_else(|| {
GatewayError::NotFound(format!("Member {} not found in team {}", user_id, team_id))
})
}
pub async fn update_member_role(
&self,
team_id: Uuid,
user_id: Uuid,
request: UpdateRoleRequest,
) -> Result<TeamMember> {
info!(
"Updating role for member {} in team {} to {:?}",
user_id, team_id, request.role
);
let _member = self.get_member(team_id, user_id).await?;
if matches!(
request.role,
TeamRole::Member | TeamRole::Viewer | TeamRole::Manager
) {
let members = self.repository.list_members(team_id).await?;
let owner_count = members
.iter()
.filter(|m| matches!(m.role, TeamRole::Owner))
.count();
let current_member = self.get_member(team_id, user_id).await?;
if matches!(current_member.role, TeamRole::Owner) && owner_count <= 1 {
return Err(GatewayError::Validation(
"Cannot remove the last owner from the team".to_string(),
));
}
}
let updated = self
.repository
.update_member_role(team_id, user_id, request.role)
.await?;
info!(
"Member {} role updated to {:?} in team {}",
user_id, updated.role, team_id
);
Ok(updated)
}
pub async fn remove_member(&self, team_id: Uuid, user_id: Uuid) -> Result<()> {
info!("Removing member {} from team {}", user_id, team_id);
let member = self.get_member(team_id, user_id).await?;
if matches!(member.role, TeamRole::Owner) {
let members = self.repository.list_members(team_id).await?;
let owner_count = members
.iter()
.filter(|m| matches!(m.role, TeamRole::Owner))
.count();
if owner_count <= 1 {
return Err(GatewayError::Validation(
"Cannot remove the last owner from the team".to_string(),
));
}
}
self.repository.remove_member(team_id, user_id).await?;
info!("Member {} removed from team {}", user_id, team_id);
Ok(())
}
pub async fn list_members(&self, team_id: Uuid) -> Result<Vec<TeamMember>> {
debug!("Listing members of team {}", team_id);
let _team = self.get_team(team_id).await?;
self.repository.list_members(team_id).await
}
pub async fn get_user_teams(&self, user_id: Uuid) -> Result<Vec<Team>> {
debug!("Fetching teams for user {}", user_id);
self.repository.get_user_teams(user_id).await
}
pub async fn get_team_usage(&self, team_id: Uuid) -> Result<TeamUsageStats> {
debug!("Fetching usage stats for team {}", team_id);
let team = self.get_team(team_id).await?;
let members = self.repository.list_members(team_id).await?;
let budget_usage_percent = team.billing.as_ref().and_then(|b| {
b.monthly_budget
.map(|budget| (b.current_usage / budget) * 100.0)
});
let remaining_budget = team.remaining_budget();
Ok(TeamUsageStats {
team_id: team.id(),
team_name: team.name,
total_requests: team.usage_stats.total_requests,
total_tokens: team.usage_stats.total_tokens,
total_cost: team.usage_stats.total_cost,
requests_today: team.usage_stats.requests_today,
tokens_today: team.usage_stats.tokens_today,
cost_today: team.usage_stats.cost_today,
member_count: members.len(),
budget_usage_percent,
remaining_budget,
})
}
pub async fn update_settings(&self, team_id: Uuid, settings: TeamSettings) -> Result<Team> {
info!("Updating settings for team {}", team_id);
let mut team = self.get_team(team_id).await?;
team.settings = settings;
self.repository.update(team).await
}
pub async fn check_user_role(
&self,
team_id: Uuid,
user_id: Uuid,
required_roles: &[TeamRole],
) -> Result<bool> {
let member = self.repository.get_member(team_id, user_id).await?;
match member {
Some(m) => {
let has_role = required_roles
.iter()
.any(|r| std::mem::discriminant(r) == std::mem::discriminant(&m.role));
Ok(has_role)
}
None => Ok(false),
}
}
pub async fn is_team_admin(&self, team_id: Uuid, user_id: Uuid) -> Result<bool> {
self.check_user_role(team_id, user_id, &[TeamRole::Owner, TeamRole::Admin])
.await
}
fn validate_team_name(&self, name: &str) -> Result<()> {
if name.is_empty() {
return Err(GatewayError::Validation(
"Team name cannot be empty".to_string(),
));
}
if name.len() > 100 {
return Err(GatewayError::Validation(
"Team name cannot exceed 100 characters".to_string(),
));
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(GatewayError::Validation(
"Team name can only contain alphanumeric characters, hyphens, and underscores"
.to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::teams::repository::InMemoryTeamRepository;
fn create_manager() -> TeamManager {
let repo = Arc::new(InMemoryTeamRepository::new());
TeamManager::new(repo)
}
#[tokio::test]
async fn test_create_team() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "test-team".to_string(),
display_name: Some("Test Team".to_string()),
description: Some("A test team".to_string()),
settings: None,
};
let team = manager.create_team(request).await.unwrap();
assert_eq!(team.name, "test-team");
assert_eq!(team.display_name, Some("Test Team".to_string()));
}
#[tokio::test]
async fn test_create_team_invalid_name() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "invalid name with spaces".to_string(),
display_name: None,
description: None,
settings: None,
};
let result = manager.create_team(request).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_team_empty_name() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "".to_string(),
display_name: None,
description: None,
settings: None,
};
let result = manager.create_team(request).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_duplicate_team() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "duplicate".to_string(),
display_name: None,
description: None,
settings: None,
};
manager.create_team(request.clone()).await.unwrap();
let result = manager.create_team(request).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_get_team() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "get-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let created = manager.create_team(request).await.unwrap();
let fetched = manager.get_team(created.id()).await.unwrap();
assert_eq!(fetched.name, "get-test");
}
#[tokio::test]
async fn test_get_team_not_found() {
let manager = create_manager();
let result = manager.get_team(Uuid::new_v4()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_update_team() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "update-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let created = manager.create_team(request).await.unwrap();
let update_request = UpdateTeamRequest {
name: None,
display_name: Some("Updated Display".to_string()),
description: Some("Updated description".to_string()),
settings: None,
status: None,
};
let updated = manager
.update_team(created.id(), update_request)
.await
.unwrap();
assert_eq!(updated.display_name, Some("Updated Display".to_string()));
assert_eq!(updated.description, Some("Updated description".to_string()));
}
#[tokio::test]
async fn test_delete_team() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "delete-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let created = manager.create_team(request).await.unwrap();
manager.delete_team(created.id()).await.unwrap();
let result = manager.get_team(created.id()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_list_teams() {
let manager = create_manager();
for i in 0..5 {
let request = CreateTeamRequest {
name: format!("team-{}", i),
display_name: None,
description: None,
settings: None,
};
manager.create_team(request).await.unwrap();
}
let (teams, total) = manager.list_teams(0, 10).await.unwrap();
assert_eq!(teams.len(), 5);
assert_eq!(total, 5);
}
#[tokio::test]
async fn test_add_member() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "member-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
let user_id = Uuid::new_v4();
let add_request = AddMemberRequest {
user_id,
role: TeamRole::Member,
};
let member = manager
.add_member(team.id(), add_request, None)
.await
.unwrap();
assert_eq!(member.user_id, user_id);
assert!(matches!(member.role, TeamRole::Member));
}
#[tokio::test]
async fn test_update_member_role() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "role-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
let user_id = Uuid::new_v4();
let add_request = AddMemberRequest {
user_id,
role: TeamRole::Member,
};
manager
.add_member(team.id(), add_request, None)
.await
.unwrap();
let update_request = UpdateRoleRequest {
role: TeamRole::Admin,
};
let updated = manager
.update_member_role(team.id(), user_id, update_request)
.await
.unwrap();
assert!(matches!(updated.role, TeamRole::Admin));
}
#[tokio::test]
async fn test_remove_member() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "remove-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
let owner_id = Uuid::new_v4();
let owner_request = AddMemberRequest {
user_id: owner_id,
role: TeamRole::Owner,
};
manager
.add_member(team.id(), owner_request, None)
.await
.unwrap();
let member_id = Uuid::new_v4();
let member_request = AddMemberRequest {
user_id: member_id,
role: TeamRole::Member,
};
manager
.add_member(team.id(), member_request, None)
.await
.unwrap();
manager.remove_member(team.id(), member_id).await.unwrap();
let result = manager.get_member(team.id(), member_id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cannot_remove_last_owner() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "owner-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
let owner_id = Uuid::new_v4();
let add_request = AddMemberRequest {
user_id: owner_id,
role: TeamRole::Owner,
};
manager
.add_member(team.id(), add_request, None)
.await
.unwrap();
let result = manager.remove_member(team.id(), owner_id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_list_members() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "list-members-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
for _ in 0..3 {
let add_request = AddMemberRequest {
user_id: Uuid::new_v4(),
role: TeamRole::Member,
};
manager
.add_member(team.id(), add_request, None)
.await
.unwrap();
}
let members = manager.list_members(team.id()).await.unwrap();
assert_eq!(members.len(), 3);
}
#[tokio::test]
async fn test_get_team_usage() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "usage-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
let add_request = AddMemberRequest {
user_id: Uuid::new_v4(),
role: TeamRole::Member,
};
manager
.add_member(team.id(), add_request, None)
.await
.unwrap();
let usage = manager.get_team_usage(team.id()).await.unwrap();
assert_eq!(usage.team_name, "usage-test");
assert_eq!(usage.member_count, 1);
}
#[tokio::test]
async fn test_is_team_admin() {
let manager = create_manager();
let request = CreateTeamRequest {
name: "admin-check-test".to_string(),
display_name: None,
description: None,
settings: None,
};
let team = manager.create_team(request).await.unwrap();
let admin_id = Uuid::new_v4();
let member_id = Uuid::new_v4();
let admin_request = AddMemberRequest {
user_id: admin_id,
role: TeamRole::Admin,
};
manager
.add_member(team.id(), admin_request, None)
.await
.unwrap();
let member_request = AddMemberRequest {
user_id: member_id,
role: TeamRole::Member,
};
manager
.add_member(team.id(), member_request, None)
.await
.unwrap();
assert!(manager.is_team_admin(team.id(), admin_id).await.unwrap());
assert!(!manager.is_team_admin(team.id(), member_id).await.unwrap());
}
}