Files
gochat/internal/service/custom_attribute_value_service.go
T

280 lines
9.5 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"gorm.io/datatypes"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
applogger "github.com/gochat/gochat/pkg/logger"
)
// CustomAttributeValueService handles setting and removing custom attribute values
// on conversations and contacts.
// Reference: Chatwoot app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb
// The value operations merge/remove individual keys into the CustomAttributes jsonb field.
type CustomAttributeValueService struct {
defRepo *repository.CustomAttributeDefinitionRepo
convRepo *repository.ConversationRepo
contactRepo *repository.ContactRepo
}
// NewCustomAttributeValueService creates a new service.
func NewCustomAttributeValueService(
defRepo *repository.CustomAttributeDefinitionRepo,
convRepo *repository.ConversationRepo,
contactRepo *repository.ContactRepo,
) *CustomAttributeValueService {
return &CustomAttributeValueService{
defRepo: defRepo,
convRepo: convRepo,
contactRepo: contactRepo,
}
}
// SetAttributeValueRequest is the DTO for setting a custom attribute value.
type SetAttributeValueRequest struct {
AttributeName string `json:"attribute_name" validate:"required,min=1"`
Value interface{} `json:"value" validate:"required"`
}
// validateAttributeDefinition checks that the attribute definition exists for the account and model.
func (s *CustomAttributeValueService) validateAttributeDefinition(ctx context.Context, accountID uint, attributeName, attributeModel string) (*model.CustomAttributeDefinition, error) {
normalizedModel := normalizeAttributeModelValue(attributeModel)
defs, _, err := s.defRepo.FindByAccountAndModel(ctx, accountID, normalizedModel, 0, 1000)
if err != nil {
return nil, err
}
if len(defs) == 0 && normalizedModel != attributeModel {
defs, _, err = s.defRepo.FindByAccountAndModel(ctx, accountID, attributeModel, 0, 1000)
if err != nil {
return nil, err
}
}
for _, def := range defs {
if def.AttributeName == attributeName {
return &def, nil
}
}
return nil, errors.New("custom attribute definition not found for this model type")
}
// mergeIntoJSON merges a single key-value pair into an existing datatypes.JSON object.
// If the existing JSON is nil or empty, it creates a new map.
func mergeIntoJSON(existing datatypes.JSON, key string, value interface{}) (datatypes.JSON, error) {
var mapData map[string]interface{}
if existing != nil && len(existing) > 0 {
if err := json.Unmarshal(existing, &mapData); err != nil {
// If unmarshal fails, start fresh
mapData = make(map[string]interface{})
}
} else {
mapData = make(map[string]interface{})
}
mapData[key] = value
result, err := json.Marshal(mapData)
if err != nil {
return nil, err
}
return datatypes.JSON(result), nil
}
// removeFromJSON removes a single key from an existing datatypes.JSON object.
// If the key doesn't exist, the JSON is returned unchanged.
func removeFromJSON(existing datatypes.JSON, key string) (datatypes.JSON, error) {
if existing == nil || len(existing) == 0 {
return datatypes.JSON("{}"), nil
}
var mapData map[string]interface{}
if err := json.Unmarshal(existing, &mapData); err != nil {
return nil, err
}
delete(mapData, key)
result, err := json.Marshal(mapData)
if err != nil {
return nil, err
}
return datatypes.JSON(result), nil
}
// SetConversationAttributeValue sets a custom attribute value on a conversation.
// It validates the attribute definition exists for the account and "conversation" model,
// then merges the value into the conversation's CustomAttributes jsonb field.
func (s *CustomAttributeValueService) SetConversationAttributeValue(ctx context.Context, accountID, conversationID uint, req *SetAttributeValueRequest) (*model.Conversation, error) {
// Validate attribute definition exists for conversation model
def, err := s.validateAttributeDefinition(ctx, accountID, req.AttributeName, "conversation")
if err != nil {
return nil, err
}
// Validate value type matches attribute_type
if err := validateValueType(def.AttributeType, req.Value); err != nil {
return nil, err
}
// Fetch the conversation
conversation, err := s.convRepo.FindByAccountAndID(ctx, accountID, conversationID)
if err != nil {
return nil, err
}
// Merge the new attribute value into existing custom attributes
merged, mergeErr := mergeIntoJSON(conversation.CustomAttributes, req.AttributeName, req.Value)
if mergeErr != nil {
applogger.L().Errorf("Merge custom attribute value failed: %v", mergeErr)
return nil, mergeErr
}
if err := s.convRepo.UpdateCustomAttributes(ctx, conversationID, merged); err != nil {
applogger.L().Errorf("Update conversation custom attributes failed: %v", err)
return nil, err
}
// Re-fetch to return updated state
conversation, err = s.convRepo.FindByAccountAndID(ctx, accountID, conversationID)
if err != nil {
return nil, err
}
return conversation, nil
}
// RemoveConversationAttributeValue removes a custom attribute key from a conversation.
func (s *CustomAttributeValueService) RemoveConversationAttributeValue(ctx context.Context, accountID, conversationID uint, attributeName string) (*model.Conversation, error) {
// Validate attribute definition exists
if _, err := s.validateAttributeDefinition(ctx, accountID, attributeName, "conversation"); err != nil {
return nil, err
}
// Fetch the conversation
conversation, err := s.convRepo.FindByAccountAndID(ctx, accountID, conversationID)
if err != nil {
return nil, err
}
// Remove the key from custom attributes
updated, removeErr := removeFromJSON(conversation.CustomAttributes, attributeName)
if removeErr != nil {
applogger.L().Errorf("Remove custom attribute value failed: %v", removeErr)
return nil, removeErr
}
if err := s.convRepo.UpdateCustomAttributes(ctx, conversationID, updated); err != nil {
applogger.L().Errorf("Update conversation custom attributes failed: %v", err)
return nil, err
}
// Re-fetch to return updated state
conversation, err = s.convRepo.FindByAccountAndID(ctx, accountID, conversationID)
if err != nil {
return nil, err
}
return conversation, nil
}
// SetContactAttributeValue sets a custom attribute value on a contact.
func (s *CustomAttributeValueService) SetContactAttributeValue(ctx context.Context, accountID, contactID uint, req *SetAttributeValueRequest) (*model.Contact, error) {
// Validate attribute definition exists for contact model
def, err := s.validateAttributeDefinition(ctx, accountID, req.AttributeName, "contact")
if err != nil {
return nil, err
}
// Validate value type matches attribute_type
if err := validateValueType(def.AttributeType, req.Value); err != nil {
return nil, err
}
// Fetch the contact
contact, err := s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
if err != nil {
return nil, err
}
// Merge the new attribute value into existing custom attributes
merged, mergeErr := mergeIntoJSON(contact.CustomAttributes, req.AttributeName, req.Value)
if mergeErr != nil {
applogger.L().Errorf("Merge custom attribute value failed: %v", mergeErr)
return nil, mergeErr
}
if err := s.contactRepo.UpdateCustomAttributes(ctx, contactID, merged); err != nil {
applogger.L().Errorf("Update contact custom attributes failed: %v", err)
return nil, err
}
// Re-fetch to return updated state
contact, err = s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
if err != nil {
return nil, err
}
return contact, nil
}
// RemoveContactAttributeValue removes a custom attribute key from a contact.
func (s *CustomAttributeValueService) RemoveContactAttributeValue(ctx context.Context, accountID, contactID uint, attributeName string) (*model.Contact, error) {
// Validate attribute definition exists
if _, err := s.validateAttributeDefinition(ctx, accountID, attributeName, "contact"); err != nil {
return nil, err
}
// Fetch the contact
contact, err := s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
if err != nil {
return nil, err
}
// Remove the key from custom attributes
updated, removeErr := removeFromJSON(contact.CustomAttributes, attributeName)
if removeErr != nil {
applogger.L().Errorf("Remove custom attribute value failed: %v", removeErr)
return nil, removeErr
}
if err := s.contactRepo.UpdateCustomAttributes(ctx, contactID, updated); err != nil {
applogger.L().Errorf("Update contact custom attributes failed: %v", err)
return nil, err
}
// Re-fetch to return updated state
contact, err = s.contactRepo.FindByAccountAndID(ctx, accountID, contactID)
if err != nil {
return nil, err
}
return contact, nil
}
// validateValueType checks that the provided value matches the expected attribute type.
func validateValueType(attributeType string, value interface{}) error {
switch attributeType {
case "text", "link":
if _, ok := value.(string); !ok {
return errors.New("value must be a string for text/link attribute type")
}
case "number":
// JSON numbers can be float or int — accept both
switch value.(type) {
case float64, int, int64, int32, float32:
// OK
default:
return errors.New("value must be a number for number attribute type")
}
case "date":
if _, ok := value.(string); !ok {
return errors.New("value must be a string (date format) for date attribute type")
}
case "checkbox":
if _, ok := value.(bool); !ok {
return errors.New("value must be a boolean for checkbox attribute type")
}
case "list":
// List can hold any JSON-serializable value
return nil
default:
return nil // Unknown types are accepted
}
return nil
}