Files
gochat/internal/service/agent_capacity_policy_service.go
T

310 lines
11 KiB
Go

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
}