feat(conversations): align search finder payload

This commit is contained in:
2026-06-07 03:32:51 +08:00
parent f001560c81
commit 9f1ff28521
4 changed files with 141 additions and 20 deletions
+8 -4
View File
@@ -49,11 +49,11 @@ Hermes task landing checklist:
## Current Baseline
- Current tracking checkpoint: 2026-06-07 P3.58 conversation index finder parity, prepared as `feat(conversations): align index finder filters`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align index finder filters`.
- Latest documentation/tooling checkpoint: this tracker update records reused `ConversationApi.get` query params through Chatwoot `ConversationsController#index` and `ConversationFinder#perform`. No route artifacts change.
- Current tracking checkpoint: 2026-06-07 P3.59 conversation search finder parity, prepared as `feat(conversations): align search finder payload`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align search finder payload`.
- Latest documentation/tooling checkpoint: this tracker update records Chatwoot `ConversationsController#search`, `ConversationFinder#perform`, and `search.json.jbuilder` behavior for the reused search route. No route artifacts change.
- Plan landing status: complete for the current known Hermes plans and user-confirmed scope. Future work should update this file directly instead of opening a parallel tracker.
- Worktree status at this implementation checkpoint: conversation index now routes reused frontend `ConversationApi.get` query params through a ConversationFinder-style path, defaulting to open conversations, applying status/inbox/team/labels/conversation_type/source/query filters before counts, applying `assignee_type` only to the returned payload, and preserving raw `{ data: { meta, payload } }`. P3.57/P3.56/P3.55 conversation meta parity, P3.54 unread-count parity, P5.8b contact import DataImportJob parity, P3.53 first-response distribution parity, and prior report/profile/channel/contact checkpoints remain implemented. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
- Worktree status at this implementation checkpoint: conversation search now routes GET `/conversations/search` through the ConversationFinder-style path too, accepts missing `q` like the reference action, returns the Chatwoot root `{ meta, payload }` search shape without the local `data` wrapper or `assigned_count`, and serializes search-specific conversation contact/inbox/message fields. P3.58 index finder parity, P3.57/P3.56/P3.55 conversation meta parity, P3.54 unread-count parity, P5.8b contact import DataImportJob parity, P3.53 first-response distribution parity, and prior report/profile/channel/contact checkpoints remain implemented. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
- Next executable implementation checkpoint: continue Phase 2/3 drift audit for deeper report data-source/metric-builder drift or the next reused-frontend mismatch, or run B12 live smoke when the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack is available. Re-run Phase 6 placeholder audit after future route/smoke changes.
- `go test ./...` passes when run outside the restricted socket sandbox for the latest implementation baseline; the latest docs/tooling checkpoint verified `scripts/parity_frontend_smoke.sh --check` with workspace-local temp/cache dirs after `/tmp` was full.
- Route dump succeeds with `967` registered routes after profile MFA route tracking.
@@ -156,6 +156,7 @@ This table is the shortest authoritative handoff view. If an older lower section
| Priority | Workstream | Current state | Next checkpoint | Commit close rule |
| --- | --- | --- | --- | --- |
| 0 | P3.59 conversation search finder parity | Implemented for reused dashboard conversation search calls: GET `/conversations/search` now follows inspected Chatwoot `ConversationsController#search`, `ConversationFinder#perform`, and `search.json.jbuilder`; it uses the finder filters/counts, accepts missing `q`, returns root `{ meta, payload }`, omits `assigned_count`, and emits search-specific contact/inbox/message payloads. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for exact model search serializer drift, PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or sender-name serialization depth. | Focused conversation search handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
| 0 | P3.58 conversation index finder parity | Implemented for reused dashboard conversation list calls: GET `/conversations` now follows inspected Chatwoot `ConversationsController#index` and `ConversationFinder#perform` for the frontend query params, including default-open status, status skip while `q` is present, pre-assignee meta counts, post-count `assignee_type` payload filtering, labels/source/query filters, and finder sort/pagination behavior. | Keep in Review; reopen from B12 conversation list smoke or fresh reference evidence for full `Conversations::PermissionFilterService` custom-role depth, PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or custom-filter POST `/conversations/filter` behavior. | Focused repository/list handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
| 0 | P3.57 conversation meta query/status parity | Implemented for reused conversation sidebar message search counts: GoChat now follows Chatwoot `ConversationFinder#set_up` by skipping status filtering whenever `q` is present, including explicit `status=open`, so query counts span all statuses before the raw `{ meta }` payload is returned. | Keep in Review; reopen from B12 conversation search smoke or fresh reference evidence for PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or full conversation list `perform` query/status drift beyond meta. | Focused repository/handler meta query/status tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
| 0 | P3.56 conversation meta query/source parity | Implemented for reused conversation sidebar search/source counts: GoChat now follows inspected Chatwoot `ConversationFinder#filter_by_query` and `#filter_by_source_id` inside meta-only counts, joining public incoming/outgoing messages for `q` and contact inboxes for `source_id` before returning raw `{ meta }`. | Keep in Review; reopen from B12 conversation sidebar smoke or fresh reference evidence for PostgreSQL `ILIKE` case-folding drift, duplicate message join count drift, or deeper custom-role permission filtering. | Focused repository/handler meta query/source tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
@@ -221,6 +222,7 @@ These rows are the executable development plan from this point forward. A checkp
| ID | Owner files | Reference files | Work to land | Exit gate |
| --- | --- | --- | --- | --- |
| P3.59 conversation search finder parity | `internal/handler/api/v1/conversation_handler.go`, `internal/handler/api/v1/conversation_serializer.go`, `internal/service/conversation_service.go`, `internal/repository/conversation_repo.go`, conversation search handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#search`, `reference/chatwoot/app/finders/conversation_finder.rb`, `reference/chatwoot/app/views/api/v1/accounts/conversations/search.json.jbuilder`, `reference/chatwoot/app/views/api/v1/models/_conversation.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js` | GET `/api/v1/accounts/:account_id/conversations/search` now uses the same finder path as index/search in Chatwoot, accepts missing `q`, preserves query/status skip semantics, and returns the root search payload shape `{ meta: { mine_count, unassigned_count, all_count }, payload }` with search-specific contact/inbox/messages fields instead of the local list/data envelope. | Review by `feat(conversations): align search finder payload`; focused search handler tests cover root payload shape, omitted `assigned_count`, missing-`q` behavior, all-status query search, and search serializer fields; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
| P3.58 conversation index finder parity | `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, `internal/repository/conversation_repo.go`, conversation CRUD/repository tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#index`, `reference/chatwoot/app/finders/conversation_finder.rb`, `reference/chatwoot/app/views/api/v1/accounts/conversations/index.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js` | GET `/api/v1/accounts/:account_id/conversations` now uses a ConversationFinder-style path for reused `ConversationApi.get` params: default status is open, `q` skips status filtering, counts are computed before `assignee_type`, payload is filtered by `assignee_type` after counts, labels/source/query filters share the meta base, and finder sort/pagination behavior replaces the old status-only list path. | Review by `feat(conversations): align index finder filters`; focused repository/list handler tests cover default-open, query/status skip, labels, pre-assignee counts, and post-count payload filtering; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
| P3.57 conversation meta query/status parity | `internal/repository/conversation_repo.go`, `internal/service/conversation_service.go`, `internal/handler/api/v1/conversation_handler.go`, conversation repository/handler tests | `reference/chatwoot/app/finders/conversation_finder.rb`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#meta`, `reference/chatwoot/app/views/api/v1/accounts/conversations/meta.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js` | Conversation meta now mirrors `ConversationFinder#set_up`: `filter_by_status` is skipped whenever `params[:q]` is present, so meta-only message-search counts include matching conversations across all statuses while preserving `q` incoming/outgoing message filtering and `source_id` filtering from P3.56. | Review by `feat(conversations): align meta query status`; focused repository/handler tests cover query counts including resolved/pending conversations despite explicit `status=open`; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
| P3.56 conversation meta query/source parity | `internal/repository/conversation_repo.go`, `internal/service/conversation_service.go`, `internal/handler/api/v1/conversation_handler.go`, conversation repository/handler tests | `reference/chatwoot/app/finders/conversation_finder.rb`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#meta`, `reference/chatwoot/app/views/api/v1/accounts/conversations/meta.json.jbuilder`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js` | Conversation meta now applies the remaining finder filters before computing count payloads: `q` joins incoming/outgoing public messages and filters content, while `source_id` joins contact inboxes and filters `contact_inboxes.source_id`; activity/template messages do not satisfy query matches, and the raw `{ meta }` response from P3.55 is preserved. | Review by `feat(conversations): filter meta query source`; focused repository/handler tests cover message query, source filtering, and activity-message exclusion; full `go test ./...` and `git diff --check` must pass. No route artifacts change. |
@@ -304,6 +306,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(conversations): align search finder payload` | Advances P3.59 with Chatwoot conversation search finder parity. GET `/conversations/search` now calls the same ConversationFinder-style service path as index, accepts missing `q`, preserves query/status-skip semantics, and returns the root search Jbuilder payload with `mine_count`, `unassigned_count`, `all_count`, and search-specific conversation contact/inbox/message fields. | `go test ./internal/handler/api/v1 -run 'ConversationCrudTestSuite/TestSearch' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'ConversationRepo_(GetMeta\|ListForFinder)\|Conversation.*(Meta\|List\|Search)\|ConversationCrudTestSuite/Test(Search\|List)' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.59 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(conversations): align index finder filters` | Advances P3.58 with Chatwoot conversation index finder parity. Reused frontend GET `/conversations` query params now flow through a ConversationFinder-style service/repository path: default-open status, status skip while `q` is present, labels/source/query filters, pre-assignee meta counts, post-count `assignee_type` payload filtering, and finder sort/pagination replace the previous status-only list behavior. | `go test ./internal/repository ./internal/handler/api/v1 -run 'ConversationRepo_(GetMeta\|ListForFinder)\|ConversationCrudTestSuite/TestList' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'Conversation.*(Meta\|List)\|ConversationRepo\|ConversationService\|ConversationCrudTestSuite' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.58 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(conversations): align meta query status` | Advances P3.57 with Chatwoot conversation meta query/status parity. `GET /conversations/meta` now follows `ConversationFinder#set_up` by skipping status filtering whenever `q` is present, even if the frontend also sends `status`, so sidebar message-search counts include matching conversations across all statuses before returning raw `{ meta }`. | `go test ./internal/repository ./internal/handler/api/v1 -run 'ConversationRepo_GetMeta\|ConversationHandlerTestSuite/TestMeta' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'Conversation.*Meta\|ConversationRepo\|ConversationService\|ConversationHandlerTestSuite' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.57 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(conversations): filter meta query source` | Advances P3.56 with remaining Chatwoot conversation meta filter parity. `GET /conversations/meta` now applies `ConversationFinder#filter_by_query` and `#filter_by_source_id` before computing raw `{ meta }` counts, joining incoming/outgoing messages for `q` and account contact inboxes for `source_id` so sidebar/search counts follow the same filtered base as the conversation finder. | `go test ./internal/repository ./internal/handler/api/v1 -run 'ConversationRepo_GetMeta\|ConversationHandlerTestSuite/TestMeta' -count=1`; `go test ./internal/repository ./internal/service ./internal/handler/api/v1 -run 'Conversation.*Meta\|ConversationRepo\|ConversationService\|ConversationHandlerTestSuite' -count=1`; `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.56 to Review; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. |
@@ -2613,3 +2616,4 @@ Verification milestone gates:
- 2026-06-07: P3.56 conversation meta query/source checkpoint prepared as `feat(conversations): filter meta query source`; audited Chatwoot `ConversationFinder#filter_by_query` and `#filter_by_source_id` plus the reused conversation meta route. GoChat meta counts now join incoming/outgoing messages for `q`, ignore activity/template messages, join contact inboxes for `source_id`, and preserve raw `{ meta }` count payloads from P3.55. Focused repository/handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
- 2026-06-07: P3.57 conversation meta query/status checkpoint prepared as `feat(conversations): align meta query status`; audited Chatwoot `ConversationFinder#set_up`, specifically `filter_by_status unless params[:q]`, plus the reused conversation meta route. GoChat meta counts now skip status filtering whenever `q` is present, including explicit `status=open`, so message-search sidebar counts include matching conversations across all statuses while preserving raw `{ meta }`. Focused repository/handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
- 2026-06-07: P3.58 conversation index finder checkpoint prepared as `feat(conversations): align index finder filters`; audited Chatwoot `ConversationsController#index`, `ConversationFinder#perform`, `index.json.jbuilder`, and reused dashboard `ConversationApi.get`. GoChat GET `/conversations` now uses the finder-style filtered base instead of the old status-only list path: default-open status, `q` status-skip, labels/source/query filters, pre-assignee meta counts, post-count `assignee_type` payload filtering, and finder sort/pagination are covered. Focused repository/list handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
- 2026-06-07: P3.59 conversation search finder checkpoint prepared as `feat(conversations): align search finder payload`; audited Chatwoot `ConversationsController#search`, `ConversationFinder#perform`, `search.json.jbuilder`, `_conversation` search model serializer, and reused dashboard `ConversationApi.search`. GoChat GET `/conversations/search` now uses the finder-style path, accepts missing `q`, keeps query/status-skip behavior, returns root `{ meta, payload }` without `assigned_count` or `data`, and serializes search-specific contact/inbox/messages fields. Focused search/list handler and finder tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
@@ -9,7 +9,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
"github.com/gochat/gochat/pkg/response"
@@ -465,22 +464,21 @@ func (h *ConversationHandler) Search(c *gin.Context) {
return
}
q := c.Query("q")
if q == "" {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "search query 'q' is required")
p := pagination.Parse(c)
var params service.FilterParams
if err := c.ShouldBindQuery(&params); err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
return
}
p := pagination.Parse(c)
searchMode := search.ParseSearchMode(c.DefaultQuery("search_mode", ""))
conversations, total, svcErr := h.conversationSvc.Search(c.Request.Context(), accountID, q, p.Offset, p.PerPage, searchMode)
result, svcErr := h.conversationSvc.ListWithFinder(c.Request.Context(), accountID, getUserID(c), params, p.Offset, p.PerPage)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
c.JSON(http.StatusOK, serializeConversationList(c.Request.Context(), h.conversationSvc.DB(), conversations, total))
c.JSON(http.StatusOK, serializeConversationSearchList(c.Request.Context(), h.conversationSvc.DB(), result.Conversations, result.Count))
}
// Filter retrieves conversations matching advanced filter criteria.
@@ -897,23 +897,44 @@ func (s *ConversationCrudTestSuite) TestUpdateLabels_NotFound() {
// ========== Search Handler Tests ==========
func (s *ConversationCrudTestSuite) TestSearch_Success() {
skipIfSQLiteForConv(s.T()) // Search uses ILIKE which SQLite doesn't support
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, Content: "needle open", MessageType: string(model.MessageTypeIncoming)}).Error)
resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"}
s.Require().NoError(s.db.Create(resolved).Error)
s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: resolved.ID, Content: "needle resolved", MessageType: string(model.MessageTypeOutgoing)}).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search?q=open", nil)
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search?q=needle&status=open", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp struct {
Meta struct {
AllCount int64 `json:"all_count"`
MineCount int64 `json:"mine_count"`
UnassignedCount int64 `json:"unassigned_count"`
AllCount int64 `json:"all_count"`
} `json:"meta"`
Payload []map[string]interface{} `json:"payload"`
Payload []struct {
ID uint `json:"id"`
Contact map[string]any `json:"contact"`
Inbox map[string]any `json:"inbox"`
Messages []map[string]any `json:"messages"`
} `json:"payload"`
}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
assert.Len(s.T(), resp.Payload, 1)
assert.NotContains(s.T(), w.Body.String(), "data")
var raw map[string]any
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &raw))
rawMeta := raw["meta"].(map[string]any)
assert.NotContains(s.T(), rawMeta, "assigned_count")
assert.Equal(s.T(), int64(2), resp.Meta.AllCount)
assert.Equal(s.T(), int64(2), resp.Meta.UnassignedCount)
assert.Len(s.T(), resp.Payload, 2)
assert.NotZero(s.T(), resp.Payload[0].ID)
assert.Equal(s.T(), s.testContact.Name, resp.Payload[0].Contact["name"])
assert.Equal(s.T(), s.testInbox.Name, resp.Payload[0].Inbox["name"])
assert.NotEmpty(s.T(), resp.Payload[0].Messages)
}
func (s *ConversationCrudTestSuite) TestSearch_InvalidAccountID() {
@@ -929,7 +950,16 @@ func (s *ConversationCrudTestSuite) TestSearch_MissingQuery() {
req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search", nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp struct {
Meta struct {
AllCount int64 `json:"all_count"`
} `json:"meta"`
Payload []map[string]any `json:"payload"`
}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
assert.Len(s.T(), resp.Payload, 1)
}
// ========== Filter Handler Tests ==========
@@ -8,6 +8,7 @@ import (
"time"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -67,6 +68,36 @@ type chatwootConversationMeta struct {
HMACVerified *bool `json:"hmac_verified,omitempty"`
}
type chatwootConversationSearchResponse struct {
Meta chatwootConversationSearchMeta `json:"meta"`
Payload []chatwootConversationSearchPayload `json:"payload"`
}
type chatwootConversationSearchMeta struct {
MineCount int64 `json:"mine_count"`
UnassignedCount int64 `json:"unassigned_count"`
AllCount int64 `json:"all_count"`
}
type chatwootConversationSearchPayload struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
CreatedAt int64 `json:"created_at"`
Contact map[string]any `json:"contact"`
Inbox map[string]any `json:"inbox"`
Messages []chatwootConversationSearchMessage `json:"messages"`
AccountID uint `json:"account_id"`
Meta chatwootConversationMeta `json:"meta"`
}
type chatwootConversationSearchMessage struct {
Content string `json:"content"`
ID uint `json:"id"`
SenderName string `json:"sender_name,omitempty"`
MessageType string `json:"message_type"`
CreatedAt int64 `json:"created_at"`
}
type chatwootMessageIndexResponse struct {
Meta chatwootMessageIndexMeta `json:"meta"`
Payload []chatwootMessagePayload `json:"payload"`
@@ -109,6 +140,64 @@ func serializeConversationList(ctx context.Context, db *gorm.DB, conversations [
}}
}
func serializeConversationSearchList(ctx context.Context, db *gorm.DB, conversations []model.Conversation, counts service.FilterCountMeta) chatwootConversationSearchResponse {
payload := make([]chatwootConversationSearchPayload, 0, len(conversations))
for i := range conversations {
payload = append(payload, serializeConversationSearch(ctx, db, &conversations[i]))
}
return chatwootConversationSearchResponse{
Meta: chatwootConversationSearchMeta{
MineCount: counts.MineCount,
UnassignedCount: counts.UnassignedCount,
AllCount: counts.AllCount,
},
Payload: payload,
}
}
func serializeConversationSearch(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationSearchPayload {
payload := chatwootConversationSearchPayload{
ID: conversationDisplayID(conversation),
UUID: conversation.UUID,
CreatedAt: conversation.CreatedAt.Unix(),
Contact: map[string]any{"id": conversation.ContactID},
Inbox: map[string]any{"id": conversation.InboxID, "channel_type": conversation.ChannelType},
Messages: []chatwootConversationSearchMessage{},
AccountID: conversation.AccountID,
Meta: serializeConversationMeta(ctx, db, conversation),
}
if db == nil {
return payload
}
var contact model.Contact
if err := db.WithContext(ctx).Where("id = ?", conversation.ContactID).First(&contact).Error; err == nil {
payload.Contact = map[string]any{"id": contact.ID, "name": contact.Name}
}
var inbox model.Inbox
if err := db.WithContext(ctx).Where("id = ?", conversation.InboxID).First(&inbox).Error; err == nil {
payload.Inbox = map[string]any{"id": inbox.ID, "name": inbox.Name, "channel_type": inbox.ChannelType}
}
var messages []model.Message
if err := db.WithContext(ctx).
Where("account_id = ? AND conversation_id = ?", conversation.AccountID, conversation.ID).
Order("id ASC").
Find(&messages).Error; err == nil {
payload.Messages = make([]chatwootConversationSearchMessage, 0, len(messages))
for i := range messages {
payload.Messages = append(payload.Messages, chatwootConversationSearchMessage{
Content: messages[i].Content,
ID: messages[i].ID,
MessageType: messages[i].MessageType,
CreatedAt: messages[i].CreatedAt.Unix(),
})
}
}
return payload
}
func serializeConversationPayloads(ctx context.Context, db *gorm.DB, conversations []model.Conversation) []chatwootConversationPayload {
payload := make([]chatwootConversationPayload, 0, len(conversations))
for i := range conversations {