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)]
#[path = "manager_tests.rs"]
mod tests;