275 lines
8.5 KiB
Go
275 lines
8.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
)
|
|
|
|
// TeamService implements business logic for Team operations.
|
|
// Reference: Chatwoot app/controllers/api/v1/teams_controller.rb
|
|
type TeamService struct {
|
|
teamRepo *repository.TeamRepo
|
|
teamMemberRepo *repository.TeamMemberRepo
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewTeamService creates a new Team service.
|
|
func NewTeamService(teamRepo *repository.TeamRepo, teamMemberRepo *repository.TeamMemberRepo, db *gorm.DB) *TeamService {
|
|
return &TeamService{teamRepo: teamRepo, teamMemberRepo: teamMemberRepo, db: db}
|
|
}
|
|
|
|
func (s *TeamService) DB() *gorm.DB {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return s.db
|
|
}
|
|
|
|
// CreateTeamRequest is the DTO for creating a team.
|
|
type CreateTeamRequest struct {
|
|
Name string `json:"name" validate:"required,min=2"`
|
|
Description string `json:"description,omitempty"`
|
|
AllowAutoAssign *bool `json:"allow_auto_assign,omitempty"`
|
|
AllowAutoAssignment *bool `json:"allow_auto_assignment,omitempty"`
|
|
}
|
|
|
|
// UpdateTeamRequest is the DTO for updating a team.
|
|
type UpdateTeamRequest struct {
|
|
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
|
|
Description string `json:"description,omitempty"`
|
|
AllowAutoAssign *bool `json:"allow_auto_assign,omitempty"`
|
|
AllowAutoAssignment *bool `json:"allow_auto_assignment,omitempty"`
|
|
}
|
|
|
|
// TeamMemberRequest is the DTO for adding/removing team members.
|
|
type TeamMemberRequest struct {
|
|
UserIDs []uint `json:"user_ids" validate:"required,min=1"`
|
|
}
|
|
|
|
// List retrieves all teams for an account.
|
|
func (s *TeamService) List(ctx context.Context, accountID uint, offset, limit int) ([]model.Team, int64, error) {
|
|
return s.teamRepo.ListByAccount(ctx, accountID, offset, limit)
|
|
}
|
|
|
|
// Get retrieves a single team by ID scoped to an account.
|
|
func (s *TeamService) Get(ctx context.Context, id, accountID uint) (*model.Team, error) {
|
|
team, err := s.teamRepo.FindByIDAndAccount(ctx, id, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("team not found: %w", err)
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
// Create creates a new team within an account.
|
|
func (s *TeamService) Create(ctx context.Context, accountID uint, req CreateTeamRequest) (*model.Team, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
autoAssign := true // default
|
|
if req.AllowAutoAssign != nil {
|
|
autoAssign = *req.AllowAutoAssign
|
|
} else if req.AllowAutoAssignment != nil {
|
|
autoAssign = *req.AllowAutoAssignment
|
|
}
|
|
|
|
team := &model.Team{
|
|
AccountID: accountID,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
AllowAutoAssignment: autoAssign,
|
|
}
|
|
|
|
if err := s.teamRepo.Create(ctx, team); err != nil {
|
|
applogger.L().Errorf("failed to create team: %v", err)
|
|
return nil, fmt.Errorf("failed to create team: %w", err)
|
|
}
|
|
if !autoAssign {
|
|
team.AllowAutoAssignment = false
|
|
if err := s.db.WithContext(ctx).Model(team).Update("allow_auto_assignment", false).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to update team auto assignment: %w", err)
|
|
}
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
// Update updates an existing team scoped to an account.
|
|
func (s *TeamService) Update(ctx context.Context, id, accountID uint, req UpdateTeamRequest) (*model.Team, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
team, err := s.teamRepo.FindByIDAndAccount(ctx, id, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("team not found: %w", err)
|
|
}
|
|
|
|
if req.Name != "" {
|
|
team.Name = req.Name
|
|
}
|
|
// Allow empty description to clear it
|
|
team.Description = req.Description
|
|
if req.AllowAutoAssign != nil {
|
|
team.AllowAutoAssignment = *req.AllowAutoAssign
|
|
} else if req.AllowAutoAssignment != nil {
|
|
team.AllowAutoAssignment = *req.AllowAutoAssignment
|
|
}
|
|
|
|
if err := s.teamRepo.Update(ctx, team); err != nil {
|
|
applogger.L().Errorf("failed to update team: %v", err)
|
|
return nil, fmt.Errorf("failed to update team: %w", err)
|
|
}
|
|
return team, nil
|
|
}
|
|
|
|
// Delete soft-deletes a team scoped to an account.
|
|
func (s *TeamService) Delete(ctx context.Context, id, accountID uint) error {
|
|
// Remove all members first
|
|
if err := s.teamMemberRepo.DeleteByTeam(ctx, id); err != nil {
|
|
applogger.L().Warnf("failed to remove team members during delete: %v", err)
|
|
}
|
|
|
|
if err := s.teamRepo.Delete(ctx, id, accountID); err != nil {
|
|
return fmt.Errorf("failed to delete team: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AddMembers adds users to a team.
|
|
func (s *TeamService) AddMembers(ctx context.Context, teamID, accountID uint, userIDs []uint) ([]model.TeamMember, error) {
|
|
// Verify team exists and belongs to account
|
|
_, err := s.teamRepo.FindByIDAndAccount(ctx, teamID, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("team not found: %w", err)
|
|
}
|
|
|
|
members := make([]model.TeamMember, 0, len(userIDs))
|
|
for _, uid := range userIDs {
|
|
// Skip if already a member
|
|
existing, _ := s.teamMemberRepo.FindByTeamAndUser(ctx, teamID, uid)
|
|
if existing != nil {
|
|
continue
|
|
}
|
|
members = append(members, model.TeamMember{
|
|
TeamID: teamID,
|
|
UserID: uid,
|
|
AvailabilityStatus: "offline",
|
|
})
|
|
}
|
|
|
|
if len(members) == 0 {
|
|
return nil, errors.New("all users are already team members")
|
|
}
|
|
|
|
if err := s.teamMemberRepo.CreateBatch(ctx, members); err != nil {
|
|
applogger.L().Errorf("failed to add team members: %v", err)
|
|
return nil, fmt.Errorf("failed to add team members: %w", err)
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// RemoveMember removes a user from a team.
|
|
func (s *TeamService) RemoveMember(ctx context.Context, teamID, userID, accountID uint) error {
|
|
// Verify team exists and belongs to account
|
|
_, err := s.teamRepo.FindByIDAndAccount(ctx, teamID, accountID)
|
|
if err != nil {
|
|
return fmt.Errorf("team not found: %w", err)
|
|
}
|
|
|
|
if err := s.teamMemberRepo.Delete(ctx, teamID, userID); err != nil {
|
|
return fmt.Errorf("failed to remove team member: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListMembers retrieves all members of a team.
|
|
func (s *TeamService) ListMembers(ctx context.Context, teamID, accountID uint) ([]model.TeamMember, error) {
|
|
// Verify team exists and belongs to account
|
|
_, err := s.teamRepo.FindByIDAndAccount(ctx, teamID, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("team not found: %w", err)
|
|
}
|
|
|
|
return s.teamMemberRepo.FindByTeam(ctx, teamID)
|
|
}
|
|
|
|
// UpdateMembers adds/removes members to match the provided user_ids list.
|
|
// Reference: Chatwoot team_members#update — calculates add/remove diffs from user_ids
|
|
func (s *TeamService) UpdateMembers(ctx context.Context, teamID, accountID uint, userIDs []uint) ([]model.TeamMember, error) {
|
|
// Get current member IDs
|
|
currentMembers, err := s.teamMemberRepo.FindByTeam(ctx, teamID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
currentIDs := make([]uint, len(currentMembers))
|
|
for i, m := range currentMembers {
|
|
currentIDs[i] = m.UserID
|
|
}
|
|
|
|
// Calculate IDs to add and remove
|
|
toAdd := difference(userIDs, currentIDs)
|
|
toRemove := difference(currentIDs, userIDs)
|
|
|
|
// Validate new user IDs belong to the account
|
|
if len(toAdd) > 0 {
|
|
// Chatwoot validates: invalid IDs (not in account) → 401
|
|
if !s.validateUserIDsBelongToAccount(ctx, accountID, toAdd) {
|
|
return nil, fmt.Errorf("unauthorized: invalid user IDs")
|
|
}
|
|
|
|
newMembers := make([]model.TeamMember, len(toAdd))
|
|
for i, uid := range toAdd {
|
|
newMembers[i] = model.TeamMember{TeamID: teamID, UserID: uid}
|
|
}
|
|
if err := s.teamMemberRepo.CreateBatch(ctx, newMembers); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Remove members not in the new list
|
|
for _, uid := range toRemove {
|
|
s.teamMemberRepo.Delete(ctx, teamID, uid)
|
|
}
|
|
|
|
// Return updated member list
|
|
return s.teamMemberRepo.FindByTeam(ctx, teamID)
|
|
}
|
|
|
|
// difference returns elements in a that are not in b.
|
|
func difference(a, b []uint) []uint {
|
|
bSet := make(map[uint]bool, len(b))
|
|
for _, v := range b {
|
|
bSet[v] = true
|
|
}
|
|
var result []uint
|
|
for _, v := range a {
|
|
if !bSet[v] {
|
|
result = append(result, v)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// validateUserIDsBelongToAccount checks that all user IDs belong to the given account.
|
|
// Reference: Chatwoot team_members#validate_member_id_params — invalid IDs → 401
|
|
func (s *TeamService) validateUserIDsBelongToAccount(ctx context.Context, accountID uint, userIDs []uint) bool {
|
|
for _, uid := range userIDs {
|
|
var count int64
|
|
s.db.WithContext(ctx).Model(&model.AccountUser{}).
|
|
Where("account_id = ? AND user_id = ?", accountID, uid).
|
|
Count(&count)
|
|
if count == 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|