Files
gochat/internal/service/custom_attribute_definition_service.go
T
2026-06-04 15:44:48 +08:00

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)
}