280 lines
9.5 KiB
Go
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
|
|
}
|