187 lines
7.9 KiB
Go
187 lines
7.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"gorm.io/datatypes"
|
|
|
|
"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"
|
|
)
|
|
|
|
// 1:1 Chatwoot: attribute_key format regex — /\A[\p{L}\p{N}_.-]+\z/
|
|
// Unicode letters/digits + underscore + dot + hyphen
|
|
var attributeKeyFormatRegex = regexp.MustCompile(`^[\p{L}\p{N}_.-]+$`)
|
|
|
|
// 1:1 Chatwoot: STANDARD_ATTRIBUTES — reserved attribute keys per model
|
|
var StandardAttributes = map[string][]string{
|
|
"conversation": {"status", "priority", "assignee_id", "inbox_id", "team_id", "display_id", "campaign_id", "labels", "browser_language", "country_code", "referer", "created_at", "last_activity_at"},
|
|
"contact": {"name", "email", "phone_number", "identifier", "country_code", "city", "company_name", "created_at", "last_activity_at", "referer", "blocked"},
|
|
"company": {"name", "domain", "description", "contacts_count", "created_at", "updated_at", "last_activity_at"},
|
|
}
|
|
|
|
// CustomAttributeDefinitionService implements business logic for custom attribute definitions.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb
|
|
type CustomAttributeDefinitionService struct {
|
|
repo *repository.CustomAttributeDefinitionRepo
|
|
}
|
|
|
|
// NewCustomAttributeDefinitionService creates a new service.
|
|
func NewCustomAttributeDefinitionService(repo *repository.CustomAttributeDefinitionRepo) *CustomAttributeDefinitionService {
|
|
return &CustomAttributeDefinitionService{repo: repo}
|
|
}
|
|
|
|
// CreateCustomAttributeDefinitionRequest is the DTO for creating a definition.
|
|
// Chatwoot compatibility: uses attribute_key, attribute_display_type, attribute_description
|
|
type CreateCustomAttributeDefinitionRequest struct {
|
|
AttributeKey string `json:"attribute_key" validate:"required,min=1"` // Chatwoot: attribute_key (maps to attribute_name)
|
|
AttributeDisplayName string `json:"attribute_display_name" validate:"required,min=1"`
|
|
AttributeDisplayType string `json:"attribute_display_type" validate:"required,oneof=text number date checkbox list link currency percent"` // 1:1 Chatwoot: added currency, percent
|
|
AttributeModel string `json:"attribute_model" validate:"required,oneof=conversation contact company"` // 1:1 Chatwoot: added company_attribute
|
|
DefaultValue datatypes.JSON `json:"default_value,omitempty"`
|
|
AttributeValues datatypes.JSON `json:"attribute_values,omitempty"` // Predefined valid values for list-type attributes
|
|
RegexPattern string `json:"regex_pattern,omitempty"` // Regex validation pattern
|
|
RegexCue string `json:"regex_cue,omitempty"` // Regex validation hint
|
|
AttributeDescription string `json:"attribute_description,omitempty"`
|
|
}
|
|
|
|
// UpdateCustomAttributeDefinitionRequest is the DTO for updating a definition.
|
|
// Chatwoot compatibility: uses attribute_key, attribute_display_type, attribute_description
|
|
type UpdateCustomAttributeDefinitionRequest struct {
|
|
AttributeDisplayName string `json:"attribute_display_name,omitempty" validate:"omitempty,min=1"`
|
|
AttributeDisplayType string `json:"attribute_display_type,omitempty" validate:"omitempty,oneof=text number date checkbox list link"`
|
|
DefaultValue datatypes.JSON `json:"default_value,omitempty"`
|
|
AttributeValues datatypes.JSON `json:"attribute_values,omitempty"`
|
|
RegexPattern string `json:"regex_pattern,omitempty"`
|
|
RegexCue string `json:"regex_cue,omitempty"`
|
|
AttributeDescription string `json:"attribute_description,omitempty"`
|
|
}
|
|
|
|
// Create creates a new custom attribute definition for an account.
|
|
func (s *CustomAttributeDefinitionService) Create(ctx context.Context, accountID uint, req *CreateCustomAttributeDefinitionRequest) (*model.CustomAttributeDefinition, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1:1 Chatwoot: normalize_attribute_fields — strip whitespace
|
|
req.AttributeKey = strings.TrimSpace(req.AttributeKey)
|
|
req.AttributeDisplayName = strings.TrimSpace(req.AttributeDisplayName)
|
|
|
|
// 1:1 Chatwoot: attribute_key format validation — /\A[\p{L}\p{N}_.-]+\z/
|
|
if !attributeKeyFormatRegex.MatchString(req.AttributeKey) {
|
|
return nil, fmt.Errorf("attribute_key format invalid: must contain only letters, numbers, underscores, dots, and hyphens")
|
|
}
|
|
|
|
// 1:1 Chatwoot: attribute_must_not_conflict — key cannot be a standard attribute
|
|
modelKey := strings.TrimSuffix(req.AttributeModel, "_attribute") // "conversation", "contact", "company"
|
|
if standardAttrs, ok := StandardAttributes[modelKey]; ok {
|
|
for _, sa := range standardAttrs {
|
|
if req.AttributeKey == sa {
|
|
return nil, fmt.Errorf("attribute_key '%s' conflicts with standard attribute for %s", req.AttributeKey, modelKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
def := &model.CustomAttributeDefinition{
|
|
AccountID: accountID,
|
|
AttributeName: req.AttributeKey,
|
|
AttributeDisplayName: req.AttributeDisplayName,
|
|
AttributeType: req.AttributeDisplayType,
|
|
AttributeModel: req.AttributeModel,
|
|
DefaultValue: req.DefaultValue,
|
|
AttributeValues: req.AttributeValues,
|
|
RegexPattern: req.RegexPattern,
|
|
RegexCue: req.RegexCue,
|
|
Description: req.AttributeDescription,
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, def); err != nil {
|
|
applogger.L().Errorf("Create custom attribute definition failed: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
|
|
// Get retrieves a definition by ID, verifying it belongs to the account.
|
|
func (s *CustomAttributeDefinitionService) Get(ctx context.Context, accountID, id uint) (*model.CustomAttributeDefinition, error) {
|
|
def, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if def.AccountID != accountID {
|
|
return nil, errors.New("custom attribute definition not found")
|
|
}
|
|
return def, nil
|
|
}
|
|
|
|
// List retrieves definitions for an account, optionally filtered by attribute_model.
|
|
func (s *CustomAttributeDefinitionService) List(ctx context.Context, accountID uint, attributeModel string, offset, limit int) ([]model.CustomAttributeDefinition, int64, error) {
|
|
if attributeModel != "" {
|
|
return s.repo.FindByAccountAndModel(ctx, accountID, attributeModel, offset, limit)
|
|
}
|
|
return s.repo.FindByAccount(ctx, accountID, offset, limit)
|
|
}
|
|
|
|
// Update modifies a definition, verifying it belongs to the account.
|
|
func (s *CustomAttributeDefinitionService) Update(ctx context.Context, accountID, id uint, req *UpdateCustomAttributeDefinitionRequest) (*model.CustomAttributeDefinition, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
def, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if def.AccountID != accountID {
|
|
return nil, errors.New("custom attribute definition not found")
|
|
}
|
|
|
|
if req.AttributeDisplayName != "" {
|
|
def.AttributeDisplayName = req.AttributeDisplayName
|
|
}
|
|
if req.AttributeDisplayType != "" {
|
|
def.AttributeType = req.AttributeDisplayType
|
|
}
|
|
if req.DefaultValue != nil {
|
|
def.DefaultValue = req.DefaultValue
|
|
}
|
|
if req.AttributeValues != nil {
|
|
def.AttributeValues = req.AttributeValues
|
|
}
|
|
if req.RegexPattern != "" {
|
|
def.RegexPattern = req.RegexPattern
|
|
}
|
|
if req.RegexCue != "" {
|
|
def.RegexCue = req.RegexCue
|
|
}
|
|
if req.AttributeDescription != "" {
|
|
def.Description = req.AttributeDescription
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, def); err != nil {
|
|
applogger.L().Errorf("Update custom attribute definition failed: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
|
|
// Delete soft-deletes a definition, verifying it belongs to the account.
|
|
func (s *CustomAttributeDefinitionService) Delete(ctx context.Context, accountID, id uint) error {
|
|
def, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if def.AccountID != accountID {
|
|
return errors.New("custom attribute definition not found")
|
|
}
|
|
|
|
return s.repo.Delete(ctx, id)
|
|
} |