Files
gochat/internal/service/assignment_policy_service.go
T

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
}