From fb775b76f678153dfdbd5e4c8c598252eddff3e6 Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 5 Jun 2026 04:05:01 +0800 Subject: [PATCH] feat(crm): align nested conversation payloads --- internal/handler/api/v1/company_handler.go | 7 ++-- .../handler/api/v1/company_handler_test.go | 36 +++++++++++++++++++ internal/handler/api/v1/contact_handler.go | 2 +- .../api/v1/contact_handler_crud_test.go | 28 +++++++++++++-- .../handler/api/v1/conversation_serializer.go | 8 +++++ internal/repository/conversation_repo.go | 2 +- 6 files changed, 74 insertions(+), 9 deletions(-) diff --git a/internal/handler/api/v1/company_handler.go b/internal/handler/api/v1/company_handler.go index 43a34cf5..e8a5f80e 100644 --- a/internal/handler/api/v1/company_handler.go +++ b/internal/handler/api/v1/company_handler.go @@ -265,16 +265,13 @@ func (h *CompanyHandler) ListConversations(c *gin.Context) { return } - pg := pagination.Parse(c) - - conversations, total, svcErr := h.svc.ListConversations(c.Request.Context(), uint(companyID), accountID, pg.Offset, pg.PerPage) + conversations, _, svcErr := h.svc.ListConversations(c.Request.Context(), uint(companyID), accountID, 0, 20) if svcErr != nil { handleServiceError(c, svcErr) return } - _ = total - c.JSON(http.StatusOK, gin.H{"payload": conversations}) + c.JSON(http.StatusOK, gin.H{"payload": serializeConversationPayloads(c.Request.Context(), h.svc.DB(), conversations)}) } func (h *CompanyHandler) SearchContacts(c *gin.Context) { diff --git a/internal/handler/api/v1/company_handler_test.go b/internal/handler/api/v1/company_handler_test.go index aa6cc372..31e5cd3c 100644 --- a/internal/handler/api/v1/company_handler_test.go +++ b/internal/handler/api/v1/company_handler_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" @@ -60,6 +61,8 @@ func (s *CompanyHandlerTestSuite) SetupSuite() { &model.Contact{}, &model.Inbox{}, &model.Conversation{}, + &model.Message{}, + &model.Attachment{}, )) s.db = db @@ -129,6 +132,10 @@ func (s *CompanyHandlerTestSuite) TearDownSuite() { func (s *CompanyHandlerTestSuite) SetupTest() { // Clean company-related tables between tests + s.db.Exec("DELETE FROM attachments") + s.db.Exec("DELETE FROM messages") + s.db.Exec("DELETE FROM conversations") + s.db.Exec("DELETE FROM contacts") s.db.Exec("DELETE FROM company_notes") s.db.Exec("DELETE FROM company_contacts") s.db.Exec("DELETE FROM companies") @@ -379,9 +386,38 @@ func (s *CompanyHandlerTestSuite) TestListConversations_Success() { companyRepo := repository.NewCompanyRepo(s.db) company := &model.Company{AccountID: s.accountID, Name: "ConvsCorp"} s.Require().NoError(companyRepo.Create(context.Background(), company)) + contact := &model.Contact{AccountID: s.accountID, CompanyID: &company.ID, Name: "Company Contact", Email: "company-contact@example.com"} + s.Require().NoError(s.db.Create(contact).Error) + inbox := &model.Inbox{AccountID: s.accountID, Name: "Company Inbox", ChannelType: "web_widget", ChannelID: 1} + s.Require().NoError(s.db.Create(inbox).Error) + + now := time.Now().Unix() + conversation := &model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: contact.ID, Status: "open", Priority: "urgent", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now} + s.Require().NoError(s.db.Create(conversation).Error) + senderID := contact.ID + s.Require().NoError(s.db.Create(&model.Message{AccountID: s.accountID, InboxID: inbox.ID, ConversationID: conversation.ID, SenderID: &senderID, SenderType: "contact", MessageType: "incoming", ContentType: "text", Status: "sent", Content: "Company scoped message"}).Error) w := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/companies/%d/conversations", s.accountID, company.ID), nil) assert.Equal(s.T(), http.StatusOK, w.Code) + + var resp map[string]interface{} + s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) + payload := resp["payload"].([]interface{}) + s.Require().Len(payload, 1) + first := payload[0].(map[string]interface{}) + assert.Equal(s.T(), "urgent", first["priority"]) + assert.Equal(s.T(), float64(conversation.ID), first["id"]) + assert.Equal(s.T(), float64(s.accountID), first["account_id"]) + assert.NotContains(s.T(), first, "contact_id") + + meta := first["meta"].(map[string]interface{}) + assert.Equal(s.T(), "web_widget", meta["channel"]) + assert.Equal(s.T(), float64(contact.ID), meta["sender"].(map[string]interface{})["id"]) + messages := first["messages"].([]interface{}) + s.Require().Len(messages, 1) + message := messages[0].(map[string]interface{}) + assert.Equal(s.T(), "Company scoped message", message["content"]) + assert.Equal(s.T(), float64(0), message["message_type"]) } // ========== ListNotes ========== diff --git a/internal/handler/api/v1/contact_handler.go b/internal/handler/api/v1/contact_handler.go index 2ac4c7f4..6dd62128 100644 --- a/internal/handler/api/v1/contact_handler.go +++ b/internal/handler/api/v1/contact_handler.go @@ -416,7 +416,7 @@ func (h *ContactHandler) ListConversations(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"payload": conversations}) + c.JSON(http.StatusOK, gin.H{"payload": serializeConversationPayloads(c.Request.Context(), h.svc.DB(), conversations)}) } // ListNotes retrieves notes for a contact. diff --git a/internal/handler/api/v1/contact_handler_crud_test.go b/internal/handler/api/v1/contact_handler_crud_test.go index 9198a855..088987a7 100644 --- a/internal/handler/api/v1/contact_handler_crud_test.go +++ b/internal/handler/api/v1/contact_handler_crud_test.go @@ -57,6 +57,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupSuite() { &model.Notification{}, &model.Conversation{}, &model.Message{}, + &model.Attachment{}, &model.ContactInbox{}, &model.InboxMember{}, &model.ContactNote{}, @@ -134,6 +135,7 @@ func (s *ContactHandlerCRUDTestSuite) SetupTest() { s.db.Exec("DELETE FROM contact_exports") s.db.Exec("DELETE FROM data_imports") s.db.Exec("DELETE FROM notifications") + s.db.Exec("DELETE FROM attachments") s.db.Exec("DELETE FROM messages") s.db.Exec("DELETE FROM notes") s.db.Exec("DELETE FROM contact_inboxes") @@ -852,8 +854,12 @@ func (s *ContactHandlerCRUDTestSuite) TestListConversations_Success() { now := time.Now().Unix() older := now - 60 - s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "low", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &older}).Error) - s.Require().NoError(s.db.Create(&model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "high", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}).Error) + olderConversation := &model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "low", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &older} + recentConversation := &model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: s.contact.ID, Status: "open", Priority: "high", ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now} + s.Require().NoError(s.db.Create(olderConversation).Error) + s.Require().NoError(s.db.Create(recentConversation).Error) + senderID := s.contact.ID + s.Require().NoError(s.db.Create(&model.Message{AccountID: s.account.ID, InboxID: inbox.ID, ConversationID: recentConversation.ID, SenderID: &senderID, SenderType: "contact", MessageType: "incoming", ContentType: "text", Status: "sent", Content: "Latest contact message"}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/contacts/%d/conversations", s.account.ID, s.contact.ID), nil) @@ -867,6 +873,24 @@ func (s *ContactHandlerCRUDTestSuite) TestListConversations_Success() { s.Len(payload, 2) first := payload[0].(map[string]interface{}) s.Equal("high", first["priority"]) + s.Contains(first, "meta") + s.Contains(first, "messages") + s.Contains(first, "last_non_activity_message") + s.Equal(float64(recentConversation.ID), first["id"]) + s.Equal(float64(s.account.ID), first["account_id"]) + s.Equal(float64(inbox.ID), first["inbox_id"]) + s.NotContains(first, "contact_id") + + meta := first["meta"].(map[string]interface{}) + s.Equal("web_widget", meta["channel"]) + s.Equal(float64(s.contact.ID), meta["sender"].(map[string]interface{})["id"]) + + messages := first["messages"].([]interface{}) + s.Len(messages, 1) + message := messages[0].(map[string]interface{}) + s.Equal("Latest contact message", message["content"]) + s.Equal(float64(0), message["message_type"]) + s.Equal(float64(recentConversation.ID), message["conversation_id"]) } func (s *ContactHandlerCRUDTestSuite) TestListConversations_InvalidContactID() { diff --git a/internal/handler/api/v1/conversation_serializer.go b/internal/handler/api/v1/conversation_serializer.go index 2a4ded6e..c9691081 100644 --- a/internal/handler/api/v1/conversation_serializer.go +++ b/internal/handler/api/v1/conversation_serializer.go @@ -108,6 +108,14 @@ func serializeConversationList(ctx context.Context, db *gorm.DB, conversations [ }} } +func serializeConversationPayloads(ctx context.Context, db *gorm.DB, conversations []model.Conversation) []chatwootConversationPayload { + payload := make([]chatwootConversationPayload, 0, len(conversations)) + for i := range conversations { + payload = append(payload, serializeConversation(ctx, db, &conversations[i])) + } + return payload +} + func serializeConversation(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationPayload { var lastMessage *model.Message if db != nil { diff --git a/internal/repository/conversation_repo.go b/internal/repository/conversation_repo.go index c7276b9a..51631bfd 100644 --- a/internal/repository/conversation_repo.go +++ b/internal/repository/conversation_repo.go @@ -203,7 +203,7 @@ func (r *ConversationRepo) FindByContactIDs(ctx context.Context, accountID uint, err := r.db.WithContext(ctx). Where("account_id = ? AND contact_id IN ?", accountID, contactIDs). - Offset(offset).Limit(limit).Order("id DESC"). + Offset(offset).Limit(limit).Order("last_activity_at DESC").Order("id DESC"). Find(&conversations).Error return conversations, total, err }