package service import ( "context" "encoding/json" "errors" "fmt" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" "gorm.io/gorm" ) // AgentCapacityPolicyService implements business logic for AgentCapacityPolicy operations. // Reference: Chatwoot AgentCapacityPolicy + P2B M1 spec type AgentCapacityPolicyService struct { repo *repository.AgentCapacityPolicyRepo } // NewAgentCapacityPolicyService creates a new AgentCapacityPolicy service. func NewAgentCapacityPolicyService(repo *repository.AgentCapacityPolicyRepo) *AgentCapacityPolicyService { return &AgentCapacityPolicyService{repo: repo} } // CreateAgentCapacityPolicyRequest is the DTO for creating an agent capacity policy. // Request body uses Chatwoot-style wrapper: { "agent_capacity_policy": { ... } } type CreateAgentCapacityPolicyRequest struct { Name string `json:"name" validate:"required"` Description string `json:"description,omitempty"` AssignmentLogic string `json:"assignment_logic,omitempty"` ExclusionRules json.RawMessage `json:"exclusion_rules,omitempty"` } // UpdateAgentCapacityPolicyRequest is the DTO for updating an agent capacity policy. // Request body uses Chatwoot-style wrapper: { "agent_capacity_policy": { ... } } type UpdateAgentCapacityPolicyRequest struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` AssignmentLogic string `json:"assignment_logic,omitempty"` ExclusionRules json.RawMessage `json:"exclusion_rules,omitempty"` } type CreateInboxCapacityLimitRequest struct { InboxID uint `json:"inbox_id"` ConversationLimit int `json:"conversation_limit"` } type UpdateInboxCapacityLimitRequest struct { ConversationLimit int `json:"conversation_limit"` } type AssignCapacityPolicyUserRequest struct { UserID uint `json:"user_id"` } // validAssignmentLogic values allowed for AssignmentLogic field. var validAssignmentLogic = map[string]bool{ "round_robin": true, "least_busy": true, } // validateAssignmentLogic checks that the assignment logic is one of the allowed values. func validateAssignmentLogic(logic string) error { if logic == "" { return nil } if !validAssignmentLogic[logic] { return fmt.Errorf("invalid assignment_logic: must be one of round_robin, least_busy") } return nil } // paginate normalises page/pageSize to sane defaults and computes offset. func paginate(page, pageSize int) (offset, normPageSize int) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 25 } if pageSize > 100 { pageSize = 100 } return (page - 1) * pageSize, pageSize } // List retrieves all agent capacity policies for an account with pagination. func (s *AgentCapacityPolicyService) List(ctx context.Context, accountID uint, page, pageSize int) ([]model.AgentCapacityPolicy, int64, error) { offset, pageSize := paginate(page, pageSize) policies, total, err := s.repo.FindByAccount(ctx, accountID, offset, pageSize) if err != nil { applogger.L().Errorf("List agent capacity policies for account %d: %v", accountID, err) return nil, 0, fmt.Errorf("failed to list agent capacity policies: %w", err) } return policies, total, nil } // Create creates a new agent capacity policy within an account. func (s *AgentCapacityPolicyService) Create(ctx context.Context, accountID uint, req CreateAgentCapacityPolicyRequest) (*model.AgentCapacityPolicy, error) { if req.Name == "" { return nil, fmt.Errorf("name is required") } if len(req.Name) > 255 { return nil, fmt.Errorf("name is too long") } if err := validateAssignmentLogic(req.AssignmentLogic); err != nil { return nil, err } assignmentLogic := req.AssignmentLogic if assignmentLogic == "" { assignmentLogic = "round_robin" } exclusionRules := req.ExclusionRules if len(exclusionRules) == 0 { exclusionRules = json.RawMessage(`{}`) } policy := &model.AgentCapacityPolicy{ AccountID: accountID, Name: req.Name, Description: req.Description, AssignmentLogic: assignmentLogic, ExclusionRules: exclusionRules, } if err := s.repo.Create(ctx, policy); err != nil { applogger.L().Errorf("failed to create agent capacity policy: %v", err) return nil, fmt.Errorf("failed to create agent capacity policy: %w", err) } return policy, nil } // Update updates an existing agent capacity policy scoped to an account. func (s *AgentCapacityPolicyService) Update(ctx context.Context, id, accountID uint, req UpdateAgentCapacityPolicyRequest) (*model.AgentCapacityPolicy, error) { policy, err := s.repo.FindByID(ctx, id) if err != nil { return nil, fmt.Errorf("agent capacity policy not found: %w", err) } if policy.AccountID != accountID { return nil, fmt.Errorf("agent capacity policy not found: policy does not belong to account %d", accountID) } // Merge fields: only update non-empty values if req.Name != nil { if *req.Name == "" { return nil, fmt.Errorf("name is required") } if len(*req.Name) > 255 { return nil, fmt.Errorf("name is too long") } policy.Name = *req.Name } if req.Description != nil { policy.Description = *req.Description } if req.AssignmentLogic != "" { if err := validateAssignmentLogic(req.AssignmentLogic); err != nil { return nil, err } policy.AssignmentLogic = req.AssignmentLogic } if req.ExclusionRules != nil { policy.ExclusionRules = req.ExclusionRules } if err := s.repo.Update(ctx, policy); err != nil { applogger.L().Errorf("failed to update agent capacity policy %d: %v", id, err) return nil, fmt.Errorf("failed to update agent capacity policy: %w", err) } return policy, nil } func (s *AgentCapacityPolicyService) CreateInboxCapacityLimit(ctx context.Context, policyID, accountID uint, req CreateInboxCapacityLimitRequest) (*model.InboxCapacityLimit, error) { policy, err := s.GetByID(ctx, policyID, accountID) if err != nil { return nil, err } if req.InboxID == 0 { return nil, fmt.Errorf("inbox_id is required") } if req.ConversationLimit < 0 { return nil, fmt.Errorf("conversation_limit must be greater than or equal to 0") } if _, err := s.repo.FindInboxByAccount(ctx, accountID, req.InboxID); err != nil { return nil, fmt.Errorf("inbox not found: %w", err) } if _, err := s.repo.FindInboxCapacityLimitByInbox(ctx, policy.ID, req.InboxID); err == nil { return nil, fmt.Errorf("inbox has already been assigned to this policy") } else if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("failed to check inbox capacity limit: %w", err) } limit := &model.InboxCapacityLimit{ AgentCapacityPolicyID: policy.ID, InboxID: req.InboxID, ConversationLimit: req.ConversationLimit, } if err := s.repo.CreateInboxCapacityLimit(ctx, limit); err != nil { return nil, fmt.Errorf("failed to create inbox capacity limit: %w", err) } return s.repo.FindInboxCapacityLimitByPolicy(ctx, policy.ID, limit.ID) } func (s *AgentCapacityPolicyService) UpdateInboxCapacityLimit(ctx context.Context, policyID, accountID, limitID uint, req UpdateInboxCapacityLimitRequest) (*model.InboxCapacityLimit, error) { if _, err := s.GetByID(ctx, policyID, accountID); err != nil { return nil, err } if req.ConversationLimit < 0 { return nil, fmt.Errorf("conversation_limit must be greater than or equal to 0") } limit, err := s.repo.FindInboxCapacityLimitByPolicy(ctx, policyID, limitID) if err != nil { return nil, fmt.Errorf("inbox capacity limit not found: %w", err) } limit.ConversationLimit = req.ConversationLimit if err := s.repo.UpdateInboxCapacityLimit(ctx, limit); err != nil { return nil, fmt.Errorf("failed to update inbox capacity limit: %w", err) } return s.repo.FindInboxCapacityLimitByPolicy(ctx, policyID, limitID) } func (s *AgentCapacityPolicyService) DeleteInboxCapacityLimit(ctx context.Context, policyID, accountID, limitID uint) error { if _, err := s.GetByID(ctx, policyID, accountID); err != nil { return err } limit, err := s.repo.FindInboxCapacityLimitByPolicy(ctx, policyID, limitID) if err != nil { return fmt.Errorf("inbox capacity limit not found: %w", err) } if err := s.repo.DeleteInboxCapacityLimit(ctx, limit); err != nil { return fmt.Errorf("failed to delete inbox capacity limit: %w", err) } return nil } func (s *AgentCapacityPolicyService) ListUsers(ctx context.Context, policyID, accountID uint) ([]model.User, error) { if _, err := s.GetByID(ctx, policyID, accountID); err != nil { return nil, err } users, err := s.repo.FindPolicyUsers(ctx, accountID, policyID) if err != nil { return nil, fmt.Errorf("failed to list agent capacity policy users: %w", err) } return users, nil } func (s *AgentCapacityPolicyService) AssignUser(ctx context.Context, policyID, accountID uint, req AssignCapacityPolicyUserRequest) (*model.User, error) { if _, err := s.GetByID(ctx, policyID, accountID); err != nil { return nil, err } if req.UserID == 0 { return nil, fmt.Errorf("user_id is required") } accountUser, err := s.repo.FindAccountUser(ctx, accountID, req.UserID) if err != nil { return nil, fmt.Errorf("account user not found: %w", err) } accountUser.AgentCapacityPolicyID = &policyID if err := s.repo.UpdateAccountUser(ctx, accountUser); err != nil { return nil, fmt.Errorf("failed to assign user to capacity policy: %w", err) } return &accountUser.User, nil } func (s *AgentCapacityPolicyService) RemoveUser(ctx context.Context, policyID, accountID, userID uint) error { if _, err := s.GetByID(ctx, policyID, accountID); err != nil { return err } accountUser, err := s.repo.FindAccountUser(ctx, accountID, userID) if err != nil { return fmt.Errorf("account user not found: %w", err) } accountUser.AgentCapacityPolicyID = nil if err := s.repo.UpdateAccountUser(ctx, accountUser); err != nil { return fmt.Errorf("failed to remove user from capacity policy: %w", err) } return nil } // Delete deletes an agent capacity policy scoped to an account. func (s *AgentCapacityPolicyService) Delete(ctx context.Context, id, accountID uint) error { policy, err := s.repo.FindByID(ctx, id) if err != nil { return fmt.Errorf("agent capacity policy not found: %w", err) } if policy.AccountID != accountID { return fmt.Errorf("agent capacity policy not found: policy does not belong to account %d", accountID) } if err := s.repo.Delete(ctx, id); err != nil { applogger.L().Errorf("failed to delete agent capacity policy %d for account %d: %v", id, accountID, err) return fmt.Errorf("failed to delete agent capacity policy: %w", err) } return nil } // GetByID retrieves a single agent capacity policy scoped to an account. func (s *AgentCapacityPolicyService) GetByID(ctx context.Context, id, accountID uint) (*model.AgentCapacityPolicy, error) { policy, err := s.repo.FindByID(ctx, id) if err != nil { return nil, fmt.Errorf("agent capacity policy not found: %w", err) } if policy.AccountID != accountID { return nil, fmt.Errorf("agent capacity policy not found: policy does not belong to account %d", accountID) } return policy, nil }