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