feat(search): align conversation result payloads

This commit is contained in:
2026-06-07 08:50:26 +08:00
parent 980e926eb2
commit 0b4706694d
11 changed files with 331 additions and 21 deletions
+136 -13
View File
@@ -318,27 +318,127 @@ func serializeSearchConversations(results []search.SearchResult) []map[string]an
func serializeSearchConversation(result search.SearchResult) map[string]any {
if conv, ok := result.Data.(model.Conversation); ok {
return map[string]any{
"id": conversationDisplayID(&conv),
"account_id": conv.AccountID,
"created_at": conv.CreatedAt.Unix(),
"additional_attributes": jsonObject(conv.AdditionalAttributes),
}
return serializeSearchConversationModel(&conv)
}
if conv, ok := result.Data.(*model.Conversation); ok && conv != nil {
return map[string]any{
"id": conversationDisplayID(conv),
"account_id": conv.AccountID,
"created_at": conv.CreatedAt.Unix(),
"additional_attributes": jsonObject(conv.AdditionalAttributes),
}
return serializeSearchConversationModel(conv)
}
data := nestedSearchData(result, "conversation")
root := searchDataRoot(result)
data := nestedSearchDataFromRoot(root, "conversation")
return map[string]any{
"id": firstMapValue(data, "display_id", "id"),
"account_id": firstMapValue(data, "account_id"),
"created_at": unixFromMapValue(firstMapValue(data, "created_at", "created_at_ts")),
"additional_attributes": firstMapValue(data, "additional_attributes"),
"message": serializeSearchConversationMessageMap(firstNestedSearchData(root, data, "message")),
"contact": serializeSearchConversationContactMap(firstNestedSearchData(root, data, "contact")),
"inbox": serializeSearchConversationInboxMap(firstNestedSearchData(root, data, "inbox")),
"agent": serializeSearchConversationAgentMap(firstNestedSearchData(root, data, "agent", "assignee")),
}
}
func serializeSearchConversationModel(conv *model.Conversation) map[string]any {
return map[string]any{
"id": conversationDisplayID(conv),
"account_id": conv.AccountID,
"created_at": conv.CreatedAt.Unix(),
"additional_attributes": jsonObject(conv.AdditionalAttributes),
"message": serializeSearchConversationMessageModel(conv),
"contact": serializeSearchConversationContactModel(conv),
"inbox": serializeSearchConversationInboxModel(conv),
"agent": serializeSearchConversationAgentModel(conv),
}
}
func serializeSearchConversationMessageModel(conv *model.Conversation) map[string]any {
if len(conv.Messages) == 0 {
return map[string]any{}
}
return serializeSearchMessageModel(&conv.Messages[0])
}
func serializeSearchConversationContactModel(conv *model.Conversation) map[string]any {
if conv.Contact != nil {
return serializeSearchContactModel(conv.Contact)
}
if conv.ContactID != 0 {
return map[string]any{"id": conv.ContactID}
}
return map[string]any{}
}
func serializeSearchConversationInboxModel(conv *model.Conversation) map[string]any {
if conv.Inbox != nil {
return map[string]any{
"id": conv.Inbox.ID,
"channel_id": conv.Inbox.ChannelID,
"name": conv.Inbox.Name,
"channel_type": conv.Inbox.ChannelType,
}
}
if conv.InboxID != 0 {
return map[string]any{"id": conv.InboxID, "channel_type": conv.ChannelType}
}
return map[string]any{}
}
func serializeSearchConversationAgentModel(conv *model.Conversation) map[string]any {
if conv.Assignee != nil {
return map[string]any{
"id": conv.Assignee.ID,
"available_name": nonEmpty(conv.Assignee.DisplayName, conv.Assignee.Name),
"email": conv.Assignee.Email,
"name": conv.Assignee.Name,
"role": nonEmpty(conv.Assignee.Role, "agent"),
}
}
return map[string]any{}
}
func serializeSearchConversationMessageMap(data map[string]any) map[string]any {
if len(data) == 0 {
return map[string]any{}
}
return serializeSearchMessage(search.SearchResult{Data: map[string]any{"message": data}})
}
func serializeSearchConversationContactMap(data map[string]any) map[string]any {
if len(data) == 0 {
return map[string]any{}
}
return map[string]any{
"email": firstMapValue(data, "email"),
"id": firstMapValue(data, "id"),
"name": firstMapValue(data, "name"),
"phone_number": firstMapValue(data, "phone_number"),
"identifier": firstMapValue(data, "identifier"),
"additional_attributes": firstMapValue(data, "additional_attributes"),
"last_activity_at": unixFromMapValue(firstMapValue(data, "last_activity_at")),
}
}
func serializeSearchConversationInboxMap(data map[string]any) map[string]any {
if len(data) == 0 {
return map[string]any{}
}
return map[string]any{
"id": firstMapValue(data, "id"),
"channel_id": firstMapValue(data, "channel_id"),
"name": firstMapValue(data, "name"),
"channel_type": firstMapValue(data, "channel_type"),
}
}
func serializeSearchConversationAgentMap(data map[string]any) map[string]any {
if len(data) == 0 {
return map[string]any{}
}
return map[string]any{
"id": firstMapValue(data, "id"),
"available_name": firstMapValue(data, "available_name", "display_name", "name"),
"email": firstMapValue(data, "email"),
"name": firstMapValue(data, "name"),
"role": firstMapValue(data, "role"),
}
}
@@ -497,10 +597,21 @@ func serializeSearchArticleModel(article *model.Article) map[string]any {
}
func nestedSearchData(result search.SearchResult, key string) map[string]any {
return nestedSearchDataFromRoot(searchDataRoot(result), key)
}
func searchDataRoot(result search.SearchResult) map[string]any {
root, ok := anyMap(result.Data)
if !ok {
return map[string]any{}
}
if data, ok := anyMap(root["data"]); ok {
return data
}
return root
}
func nestedSearchDataFromRoot(root map[string]any, key string) map[string]any {
if nested, ok := anyMap(root[key]); ok {
return nested
}
@@ -513,6 +624,18 @@ func nestedSearchData(result search.SearchResult, key string) map[string]any {
return root
}
func firstNestedSearchData(root map[string]any, fallback map[string]any, keys ...string) map[string]any {
for _, key := range keys {
if nested, ok := anyMap(root[key]); ok {
return nested
}
if nested, ok := anyMap(fallback[key]); ok {
return nested
}
}
return map[string]any{}
}
func anyMap(value any) (map[string]any, bool) {
switch typed := value.(type) {
case map[string]any:
@@ -6,10 +6,12 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
@@ -198,6 +200,59 @@ func TestSearchHandler_SearchConversations_Success(t *testing.T) {
require.Len(t, results, 1)
}
func TestSearchHandler_SearchConversations_ChatwootPayloadShape(t *testing.T) {
displayID := uint(42)
createdAt := time.Date(2026, 6, 7, 8, 30, 0, 0, time.UTC)
conversation := model.Conversation{
Base: model.Base{ID: 7, CreatedAt: createdAt},
DisplayID: &displayID,
AccountID: 1,
InboxID: 3,
ContactID: 5,
AssigneeID: uintPtr(9),
AdditionalAttributes: datatypes.JSON(`{"mail_subject":"Need pricing"}`),
Contact: &model.Contact{Base: model.Base{ID: 5}, AccountID: 1, Name: "Ada", Email: "ada@example.com"},
Inbox: &model.Inbox{Base: model.Base{ID: 3}, AccountID: 1, Name: "Website", ChannelType: "web_widget", ChannelID: 11},
Assignee: &model.User{Base: model.Base{ID: 9}, AccountID: 1, Name: "Agent One", Email: "agent@example.com", Role: "administrator"},
Messages: []model.Message{{
Base: model.Base{ID: 13, CreatedAt: createdAt.Add(-time.Minute)},
AccountID: 1,
InboxID: 3,
ConversationID: 7,
Content: "hello",
MessageType: "incoming",
ContentType: "text",
Status: "sent",
}},
}
repo := &mockSearchRepo{conversations: []model.Conversation{conversation}, convTotal: 1}
svc := search.NewSearchService(repo)
handler := NewSearchHandler(svc)
router := setupSearchHandlerRouter(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/accounts/1/search/conversations?q=ada", nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]any
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
payload := body["payload"].(map[string]any)
results := payload["conversations"].([]any)
require.Len(t, results, 1)
item := results[0].(map[string]any)
assert.Equal(t, float64(42), item["id"])
assert.Equal(t, float64(createdAt.Unix()), item["created_at"])
assert.Equal(t, "Need pricing", item["additional_attributes"].(map[string]any)["mail_subject"])
assert.Equal(t, "Ada", item["contact"].(map[string]any)["name"])
assert.Equal(t, "ada@example.com", item["contact"].(map[string]any)["email"])
assert.Equal(t, "Website", item["inbox"].(map[string]any)["name"])
assert.Equal(t, float64(11), item["inbox"].(map[string]any)["channel_id"])
assert.Equal(t, "Agent One", item["agent"].(map[string]any)["available_name"])
assert.Equal(t, float64(13), item["message"].(map[string]any)["id"])
assert.Equal(t, float64(0), item["message"].(map[string]any)["message_type"])
}
func TestSearchHandler_SearchConversations_InvalidAccountID(t *testing.T) {
repo := &mockSearchRepo{}
svc := search.NewSearchService(repo)