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 }