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

113 lines
4.2 KiB
Go

package service
// ContactMergeService handles the business logic for merging two contacts.
// Reference: Chatwoot app/actions/contact_merge_action.rb
//
// The merge operation runs in a transaction:
// 1. Validate both contacts belong to the account
// 2. Move all conversations from mergee to base
// 3. Move all messages (sender=mergee) to base
// 4. Move all contact_inboxes from mergee to base
// 5. Move all notes from mergee to base
// 6. Delete the mergee contact
// 7. Return the base contact
import (
"context"
"errors"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"gorm.io/gorm"
)
var (
ErrMergeSameContact = errors.New("cannot merge the same contact")
ErrMergeBaseNotFound = errors.New("base contact not found")
ErrMergeeNotFound = errors.New("mergee contact not found")
ErrMergeNotInAccount = errors.New("contact does not belong to the account")
)
// MergeRequest represents the request to merge two contacts.
// Used by ContactHandler for JSON binding.
type MergeRequest struct {
BaseContactID uint `json:"base_contact_id" binding:"required"`
MergeeContactID uint `json:"mergee_contact_id" binding:"required"`
}
// ContactMergeService handles contact merge operations.
type ContactMergeService struct {
mergeRepo *repository.ContactMergeRepo
db *gorm.DB
}
// NewContactMergeService creates a new ContactMergeService.
func NewContactMergeService(mergeRepo *repository.ContactMergeRepo, db *gorm.DB) *ContactMergeService {
return &ContactMergeService{mergeRepo: mergeRepo, db: db}
}
// Merge performs the contact merge operation within a transaction.
// Reference: Chatwoot ContactMergeAction#perform — runs all merge steps in ActiveRecord::Base.transaction
//
// Returns the base contact after merge, or error if validation fails.
// If base_contact_id == mergee_contact_id, returns base contact immediately (Chatwoot behavior).
func (s *ContactMergeService) Merge(accountID, baseContactID, mergeeContactID uint) (*model.Contact, error) {
// Reference: Chatwoot — "return @base_contact if base_contact.id == mergee_contact.id"
if baseContactID == mergeeContactID {
return s.mergeRepo.GetBaseContact(accountID, baseContactID)
}
// Validate contacts belong to the account
if err := s.mergeRepo.ValidateContacts(accountID, baseContactID, mergeeContactID); err != nil {
if errors.Is(err, repository.ErrSameContactMerge) {
return nil, ErrMergeSameContact
}
if errors.Is(err, repository.ErrBaseContactNotFound) {
return nil, ErrMergeBaseNotFound
}
if errors.Is(err, repository.ErrMergeeContactNotFound) {
return nil, ErrMergeeNotFound
}
return nil, ErrMergeNotInAccount
}
// Run all merge operations in a transaction
err := s.db.Transaction(func(tx *gorm.DB) error {
// Use tx for all operations within the transaction
txRepo := repository.NewContactMergeRepo(tx)
if err := txRepo.MergeConversations(baseContactID, mergeeContactID); err != nil {
return err
}
if err := txRepo.MergeMessages(baseContactID, mergeeContactID); err != nil {
return err
}
if err := txRepo.MergeContactInboxes(baseContactID, mergeeContactID); err != nil {
return err
}
if err := txRepo.MergeContactNotes(baseContactID, mergeeContactID, accountID); err != nil {
return err
}
// 1:1 Chatwoot: merge_and_remove_mergee_contact
// Field priority: base_contact attributes take precedence over mergee_contact
// mergable_attribute_keys: identifier, name, email, phone_number, additional_attributes, custom_attributes
// Logic: mergee_attributes.deep_merge(base_attributes) → base_contact.update(merged)
if err := txRepo.MergeAttributesAndRemoveMergee(baseContactID, mergeeContactID, accountID); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
// Return the base contact after successful merge
return s.mergeRepo.GetBaseContact(accountID, baseContactID)
}
// MergeWithRequest handles a MergeRequest from the handler.
// Compatible with ContactHandler which calls: Merge(ctx, accountID, req)
func (s *ContactMergeService) MergeWithRequest(ctx context.Context, accountID uint, req MergeRequest) (*model.Contact, error) {
return s.Merge(accountID, req.BaseContactID, req.MergeeContactID)
}