310 lines
11 KiB
Go
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
|
|
}
|