289 lines
11 KiB
Go
289 lines
11 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/autoassignment"
|
|
"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"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// AssignmentPolicyService implements business logic for AssignmentPolicy operations.
|
|
// Reference: Chatwoot app/controllers/api/v1/assignment_policies_controller.rb
|
|
type AssignmentPolicyService struct {
|
|
policyRepo *repository.AssignmentPolicyRepo
|
|
inboxPolicyRepo *repository.InboxAssignmentPolicyRepo
|
|
assignSvc *autoassignment.AssignmentService
|
|
}
|
|
|
|
type AssignmentPolicyPayload map[string]any
|
|
|
|
// NewAssignmentPolicyService creates a new AssignmentPolicy service.
|
|
func NewAssignmentPolicyService(
|
|
policyRepo *repository.AssignmentPolicyRepo,
|
|
inboxPolicyRepo *repository.InboxAssignmentPolicyRepo,
|
|
assignSvc *autoassignment.AssignmentService,
|
|
) *AssignmentPolicyService {
|
|
return &AssignmentPolicyService{policyRepo: policyRepo, inboxPolicyRepo: inboxPolicyRepo, assignSvc: assignSvc}
|
|
}
|
|
|
|
// CreatePolicyRequest is the DTO for creating an assignment policy.
|
|
type CreatePolicyRequest struct {
|
|
Name string `json:"name" validate:"required"`
|
|
Description string `json:"description,omitempty"`
|
|
AssignmentOrder int `json:"assignment_order,omitempty"`
|
|
ConversationPriority int `json:"conversation_priority,omitempty"`
|
|
FairDistributionLimit int `json:"fair_distribution_limit,omitempty"`
|
|
FairDistributionWindow int `json:"fair_distribution_window,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
// UpdatePolicyRequest is the DTO for updating an assignment policy.
|
|
type UpdatePolicyRequest struct {
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
AssignmentOrder *int `json:"assignment_order,omitempty"`
|
|
ConversationPriority *int `json:"conversation_priority,omitempty"`
|
|
FairDistributionLimit *int `json:"fair_distribution_limit,omitempty"`
|
|
FairDistributionWindow *int `json:"fair_distribution_window,omitempty"`
|
|
Enabled *bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
// CreateInboxPolicyRequest is the DTO for creating an inbox assignment policy.
|
|
type CreateInboxPolicyRequest struct {
|
|
InboxID uint `json:"inbox_id" validate:"required"`
|
|
AssignmentPolicyID uint `json:"assignment_policy_id" validate:"required"`
|
|
}
|
|
|
|
// UpdateInboxPolicyRequest is the DTO for updating an inbox assignment policy.
|
|
type UpdateInboxPolicyRequest struct {
|
|
AssignmentPolicyID uint `json:"assignment_policy_id" validate:"required"`
|
|
}
|
|
|
|
// ListAccountPolicies retrieves all account-scoped assignment policies.
|
|
func (s *AssignmentPolicyService) ListAccountPolicies(ctx context.Context, accountID uint) ([]AssignmentPolicyPayload, error) {
|
|
policies, err := s.policyRepo.FindByAccount(ctx, accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list assignment policies: %w", err)
|
|
}
|
|
|
|
items := make([]AssignmentPolicyPayload, 0, len(policies))
|
|
for i := range policies {
|
|
payload, err := s.SerializePolicy(ctx, &policies[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, payload)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetAccountPolicy retrieves an account-scoped assignment policy by ID.
|
|
func (s *AssignmentPolicyService) GetAccountPolicy(ctx context.Context, accountID uint, policyID ...uint) (*model.AssignmentPolicy, error) {
|
|
if len(policyID) > 0 && policyID[0] != 0 {
|
|
policy, err := s.policyRepo.FindByAccountAndID(ctx, accountID, policyID[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
policies, err := s.policyRepo.FindByAccount(ctx, accountID)
|
|
if err != nil || len(policies) == 0 {
|
|
if err == nil {
|
|
err = gorm.ErrRecordNotFound
|
|
}
|
|
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
|
}
|
|
return &policies[0], nil
|
|
}
|
|
|
|
// CreateAccountPolicy creates the account-level assignment policy.
|
|
func (s *AssignmentPolicyService) CreateAccountPolicy(ctx context.Context, accountID uint, req CreatePolicyRequest) (*model.AssignmentPolicy, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
enabled := true
|
|
if req.Enabled != nil {
|
|
enabled = *req.Enabled
|
|
}
|
|
|
|
limit := 100
|
|
if req.FairDistributionLimit > 0 {
|
|
limit = req.FairDistributionLimit
|
|
}
|
|
|
|
window := 3600
|
|
if req.FairDistributionWindow > 0 {
|
|
window = req.FairDistributionWindow
|
|
}
|
|
|
|
policy := &model.AssignmentPolicy{
|
|
AccountID: accountID,
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
AssignmentOrder: req.AssignmentOrder,
|
|
ConversationPriority: req.ConversationPriority,
|
|
FairDistributionLimit: limit,
|
|
FairDistributionWindow: window,
|
|
Enabled: enabled,
|
|
}
|
|
|
|
if err := s.policyRepo.Create(ctx, policy); err != nil {
|
|
applogger.L().Errorf("failed to create assignment policy: %v", err)
|
|
return nil, fmt.Errorf("failed to create assignment policy: %w", err)
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
// UpdateAccountPolicy updates the account-level assignment policy.
|
|
func (s *AssignmentPolicyService) UpdateAccountPolicy(ctx context.Context, id, accountID uint, req UpdatePolicyRequest) (*model.AssignmentPolicy, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
policy, err := s.policyRepo.FindByAccountAndID(ctx, accountID, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
|
}
|
|
|
|
if req.Name != "" {
|
|
policy.Name = req.Name
|
|
}
|
|
policy.Description = req.Description
|
|
if req.AssignmentOrder != nil {
|
|
policy.AssignmentOrder = *req.AssignmentOrder
|
|
}
|
|
if req.ConversationPriority != nil {
|
|
policy.ConversationPriority = *req.ConversationPriority
|
|
}
|
|
if req.FairDistributionLimit != nil {
|
|
policy.FairDistributionLimit = *req.FairDistributionLimit
|
|
}
|
|
if req.FairDistributionWindow != nil {
|
|
policy.FairDistributionWindow = *req.FairDistributionWindow
|
|
}
|
|
if req.Enabled != nil {
|
|
policy.Enabled = *req.Enabled
|
|
}
|
|
|
|
if err := s.policyRepo.Update(ctx, policy); err != nil {
|
|
applogger.L().Errorf("failed to update assignment policy: %v", err)
|
|
return nil, fmt.Errorf("failed to update assignment policy: %w", err)
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
// DeleteAccountPolicy deletes the account-level assignment policy.
|
|
func (s *AssignmentPolicyService) DeleteAccountPolicy(ctx context.Context, id, accountID uint) error {
|
|
_, err := s.policyRepo.FindByAccountAndID(ctx, accountID, id)
|
|
if err != nil {
|
|
return fmt.Errorf("assignment policy not found: %w", err)
|
|
}
|
|
|
|
if err := s.policyRepo.Delete(ctx, id); err != nil {
|
|
return fmt.Errorf("failed to delete assignment policy: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetInboxPolicy retrieves the assignment policy associated with an inbox.
|
|
func (s *AssignmentPolicyService) GetInboxPolicy(ctx context.Context, accountID, inboxID uint) (*model.AssignmentPolicy, error) {
|
|
policy, err := s.inboxPolicyRepo.FindPolicyByInbox(ctx, accountID, inboxID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inbox assignment policy not found: %w", err)
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
// ListPolicyInboxes retrieves inboxes associated with an assignment policy.
|
|
func (s *AssignmentPolicyService) ListPolicyInboxes(ctx context.Context, accountID, policyID uint) ([]model.Inbox, error) {
|
|
if _, err := s.policyRepo.FindByAccountAndID(ctx, accountID, policyID); err != nil {
|
|
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
|
}
|
|
return s.inboxPolicyRepo.FindInboxesByPolicy(ctx, accountID, policyID)
|
|
}
|
|
|
|
// CreateInboxPolicy creates an inbox-level assignment policy override.
|
|
func (s *AssignmentPolicyService) CreateInboxPolicy(ctx context.Context, accountID uint, req CreateInboxPolicyRequest) (*model.AssignmentPolicy, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := s.policyRepo.FindByAccountAndID(ctx, accountID, req.AssignmentPolicyID); err != nil {
|
|
return nil, fmt.Errorf("assignment policy not found: %w", err)
|
|
}
|
|
if _, err := s.inboxPolicyRepo.ReplaceForInbox(ctx, req.InboxID, req.AssignmentPolicyID); err != nil {
|
|
return nil, fmt.Errorf("create inbox assignment policy: %w", err)
|
|
}
|
|
return s.GetInboxPolicy(ctx, accountID, req.InboxID)
|
|
}
|
|
|
|
// UpdateInboxPolicy updates an inbox-level assignment policy override.
|
|
func (s *AssignmentPolicyService) UpdateInboxPolicy(ctx context.Context, id, accountID uint, req UpdateInboxPolicyRequest) (*model.AssignmentPolicy, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.CreateInboxPolicy(ctx, accountID, CreateInboxPolicyRequest{InboxID: id, AssignmentPolicyID: req.AssignmentPolicyID})
|
|
}
|
|
|
|
// DeleteInboxPolicy deletes an inbox-level assignment policy override.
|
|
func (s *AssignmentPolicyService) DeleteInboxPolicy(ctx context.Context, id, accountID uint) error {
|
|
if _, err := s.inboxPolicyRepo.FindPolicyByInbox(ctx, accountID, id); err != nil {
|
|
return fmt.Errorf("inbox assignment policy not found: %w", err)
|
|
}
|
|
|
|
if err := s.inboxPolicyRepo.DeleteByInbox(ctx, id); err != nil {
|
|
return fmt.Errorf("failed to delete inbox assignment policy: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SerializePolicy returns Chatwoot assignment_policy partial shape.
|
|
func (s *AssignmentPolicyService) SerializePolicy(ctx context.Context, policy *model.AssignmentPolicy) (AssignmentPolicyPayload, error) {
|
|
count, err := s.policyRepo.CountAssignedInboxes(ctx, policy.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("count assigned inboxes: %w", err)
|
|
}
|
|
return AssignmentPolicyPayload{
|
|
"id": policy.ID,
|
|
"name": policy.Name,
|
|
"description": policy.Description,
|
|
"assignment_order": policy.AssignmentOrder,
|
|
"conversation_priority": policy.ConversationPriority,
|
|
"fair_distribution_limit": policy.FairDistributionLimit,
|
|
"fair_distribution_window": policy.FairDistributionWindow,
|
|
"enabled": policy.Enabled,
|
|
"assigned_inbox_count": count,
|
|
"created_at": unixSeconds(policy.CreatedAt),
|
|
"updated_at": unixSeconds(policy.UpdatedAt),
|
|
}, nil
|
|
}
|
|
|
|
func unixSeconds(value time.Time) int64 {
|
|
if value.IsZero() {
|
|
return 0
|
|
}
|
|
return value.Unix()
|
|
}
|
|
|
|
func IsAssignmentPolicyNotFound(err error) bool {
|
|
return err != nil && (errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, gorm.ErrInvalidDB))
|
|
}
|
|
|
|
// AssignConversation assigns a specific conversation to an agent using auto-assignment.
|
|
func (s *AssignmentPolicyService) AssignConversation(ctx context.Context, conversationID, inboxID, accountID uint) (uint, error) {
|
|
agentID, err := s.assignSvc.AssignConversation(ctx, conversationID, inboxID, accountID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to assign conversation: %w", err)
|
|
}
|
|
return agentID, nil
|
|
}
|