feat(crm): align contact merge action

This commit is contained in:
2026-06-05 03:07:57 +08:00
parent c7f2b43b4a
commit 1e3bf47071
10 changed files with 142 additions and 36 deletions
+1 -1
View File
@@ -818,7 +818,7 @@ func (h *ContactHandler) Merge(c *gin.Context) {
return
}
response.OK(c, result)
c.JSON(http.StatusOK, serializeCRMContact(c.Request.Context(), h.svc.DB(), result, false))
}
// Filter retrieves contacts matching advanced filter criteria.
@@ -52,6 +52,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
&model.Tag{},
&model.ContactLabel{},
&model.Conversation{},
&model.Message{},
&model.ContactInbox{},
&model.InboxMember{},
&model.ContactNote{},
@@ -92,6 +93,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() {
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/contact_inboxes", s.handler.ListContactInboxes)
s.router.GET("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.ListNotes)
s.router.POST("/api/v1/accounts/:id/contacts/:contact_id/notes", s.handler.CreateNote)
s.router.POST("/api/v1/accounts/:id/actions/contact_merge", s.handler.Merge)
// Create test data
s.account = &model.Account{Name: "Test Account", Locale: "en", Status: "active"}
@@ -118,6 +120,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupTest() {
s.db.Exec("DELETE FROM contact_notes")
s.db.Exec("DELETE FROM contact_labels")
s.db.Exec("DELETE FROM tags")
s.db.Exec("DELETE FROM messages")
s.db.Exec("DELETE FROM notes")
s.db.Exec("DELETE FROM contact_inboxes")
s.db.Exec("DELETE FROM conversations")
@@ -559,6 +562,41 @@ func (s *ContactHandlerCRUDTestSuite) TestDeleteAvatar_Success() {
s.Equal("", found.AvatarURL)
}
func (s *ContactHandlerCRUDTestSuite) TestMerge_ChatwootActionsPathReturnsRawContact() {
base := &model.Contact{AccountID: s.account.ID, Name: "Base Contact", Email: "base@example.com"}
mergee := &model.Contact{AccountID: s.account.ID, Name: "Mergee Contact", PhoneNumber: "+12212345"}
s.Require().NoError(s.db.Create(base).Error)
s.Require().NoError(s.db.Create(mergee).Error)
s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: 1, ContactID: mergee.ID, Status: "open", ChannelType: "Channel::WebWidget", Channel: "web_widget"}).Error)
s.Require().NoError(s.db.Create(&model.Note{AccountID: s.account.ID, ContactID: mergee.ID, Content: "mergee note", UserID: &s.user.ID}).Error)
bodyBytes, _ := json.Marshal(map[string]uint{"base_contact_id": base.ID, "mergee_contact_id": mergee.ID})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST",
fmt.Sprintf("/api/v1/accounts/%d/actions/contact_merge", s.account.ID),
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(float64(base.ID), resp["id"])
s.Equal("Base Contact", resp["name"])
s.Equal("base@example.com", resp["email"])
s.Equal("+12212345", resp["phone_number"])
s.NotContains(resp, "payload")
s.NotContains(resp, "success")
var count int64
s.db.Model(&model.Contact{}).Where("id = ?", mergee.ID).Count(&count)
s.Equal(int64(0), count)
s.db.Model(&model.Conversation{}).Where("contact_id = ?", base.ID).Count(&count)
s.Equal(int64(1), count)
s.db.Model(&model.Note{}).Where("contact_id = ?", base.ID).Count(&count)
s.Equal(int64(1), count)
}
func (s *ContactHandlerCRUDTestSuite) TestLabels_UpdateListAndFilter() {
bodyBytes, _ := json.Marshal(map[string]interface{}{"labels": []string{"vip", "trial"}})
@@ -12,6 +12,7 @@ package v1
// Chatwoot's response: returns the base contact after merge on success, 200 OK
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
@@ -32,12 +33,13 @@ func NewContactMergeHandler(mergeService *service.ContactMergeService) *ContactM
// Create merges the mergee contact into the base contact.
// Reference: Chatwoot `def create`
// contact_merge_action = ContactMergeAction.new(
// account: Current.account,
// base_contact: @base_contact,
// mergee_contact: @mergee_contact
// )
// contact_merge_action.perform
//
// contact_merge_action = ContactMergeAction.new(
// account: Current.account,
// base_contact: @base_contact,
// mergee_contact: @mergee_contact
// )
// contact_merge_action.perform
//
// Chatwoot returns the base contact on success with 200 OK.
func (h *ContactMergeHandler) Create(c *gin.Context) {
@@ -75,5 +77,5 @@ func (h *ContactMergeHandler) Create(c *gin.Context) {
return
}
response.OK(c, contact)
}
c.JSON(http.StatusOK, serializeCRMContact(c.Request.Context(), nil, contact, false))
}
@@ -24,7 +24,7 @@ type ContactMergeHandlerTestSuite struct {
db *gorm.DB
handler *ContactMergeHandler
account *model.Account
account *model.Account
contact1 *model.Contact
contact2 *model.Contact
}
@@ -63,31 +63,42 @@ func TestContactMergeHandlerSuite(t *testing.T) {
func (s *ContactMergeHandlerTestSuite) TestCreate_Success() {
r := gin.New()
r.POST("/api/v1/accounts/:account_id/actions/contact_merges", func(c *gin.Context) {
r.POST("/api/v1/accounts/:account_id/actions/contact_merge", func(c *gin.Context) {
c.Set("account_id", float64(s.account.ID))
c.Next()
}, s.handler.Create)
body := map[string]uint{"base_contact_id": s.contact1.ID, "mergee_contact_id": s.contact2.ID}
base := &model.Contact{Name: "merge-base", Email: "base@example.com", AccountID: s.account.ID}
mergee := &model.Contact{Name: "mergee-contact", PhoneNumber: "+12212345", AccountID: s.account.ID}
s.Require().NoError(s.db.Create(base).Error)
s.Require().NoError(s.db.Create(mergee).Error)
body := map[string]uint{"base_contact_id": base.ID, "mergee_contact_id": mergee.ID}
b, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/actions/contact_merges", s.account.ID), bytes.NewBuffer(b))
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/actions/contact_merge", s.account.ID), bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var payload map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
assert.Equal(s.T(), float64(base.ID), payload["id"])
assert.Equal(s.T(), "base@example.com", payload["email"])
assert.NotContains(s.T(), payload, "success")
assert.NotContains(s.T(), payload, "data")
}
func (s *ContactMergeHandlerTestSuite) TestCreate_BadRequest_NoBody() {
r := gin.New()
r.POST("/api/v1/accounts/:account_id/actions/contact_merges", func(c *gin.Context) {
r.POST("/api/v1/accounts/:account_id/actions/contact_merge", func(c *gin.Context) {
c.Set("account_id", float64(s.account.ID))
c.Next()
}, s.handler.Create)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/actions/contact_merges", s.account.ID), nil)
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/actions/contact_merge", s.account.ID), nil)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
@@ -96,15 +107,15 @@ func (s *ContactMergeHandlerTestSuite) TestCreate_BadRequest_NoBody() {
func (s *ContactMergeHandlerTestSuite) TestCreate_BadRequest_InvalidAccountID() {
r := gin.New()
r.POST("/api/v1/accounts/:account_id/actions/contact_merges", s.handler.Create)
r.POST("/api/v1/accounts/:account_id/actions/contact_merge", s.handler.Create)
body := map[string]uint{"base_contact_id": s.contact1.ID, "mergee_contact_id": s.contact2.ID}
b, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/actions/contact_merges", bytes.NewBuffer(b))
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/actions/contact_merge", bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
}
}