From bd20e6d80f48ae337d9cdb91e1127481b1db0cf9 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 7 Jun 2026 06:53:41 +0800 Subject: [PATCH] feat(conversations): align filter query message types --- docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md | 11 +++++--- internal/service/conversation_service.go | 14 ++++++++--- internal/service/conversation_service_test.go | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 3053ed9b..30dfd9fe 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,11 +49,11 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-07 P3.75 automation message filter parity, prepared as `feat(automation): align message filters`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(automation): align message filters`. -- Latest documentation/tooling checkpoint: this tracker update records Chatwoot automation message filter parity for `message_created` rules. No route artifacts change. +- Current tracking checkpoint: 2026-06-07 P3.76 conversation filter query message-type parity, prepared as `feat(conversations): align filter query message types`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align filter query message types`. +- Latest documentation/tooling checkpoint: this tracker update records Chatwoot conversation filter query parity for incoming/outgoing message searches. 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: automation rule matching now evaluates Chatwoot `message_created` message filters from the triggering message payload: `content` uses processed message content when available and falls back to persisted content, `private_note` maps to message private state, and `message_type` accepts Chatwoot numeric enum values plus local string values. Existing P3.74 contact type default parity, P3.73 conversation filter permission parity, P3.72 account enabled-feature serialization parity, P3.71 contact referer filter parity, P5.8c contact import search indexing, P3.70 global contact search resolved-scope parity, and prior 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 filter `q` searches now preserve Chatwoot `ConversationFinder#filter_by_query` semantics while using GoChat's persisted message type strings: query matches only incoming/outgoing messages and excludes activity/template rows. Existing P3.75 automation message filter parity, P3.74 contact type default parity, P3.73 conversation filter permission parity, P3.72 account enabled-feature serialization parity, P3.71 contact referer filter parity, P5.8c contact import search indexing, P3.70 global contact search resolved-scope parity, and prior 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 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.76 conversation filter query message-type parity | Implemented for reused conversation filter/list query flows: `q` message search now filters against persisted GoChat string message types (`incoming`, `outgoing`) while preserving the Chatwoot reference restriction to incoming/outgoing messages and excluding activity/template matches. | Keep in Review; reopen from B12 conversation search/filter smoke or fresh reference evidence for duplicate join count behavior, `processed_message_content` query drift, or exact PostgreSQL ILIKE collation differences. | Focused conversation service query filter test, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. | | 0 | P3.75 automation message filter parity | Implemented for reused automation `message_created` rules: GoChat now evaluates `message_type`, `private_note`, and `content` against the triggering message payload, including processed-content fallback and Chatwoot numeric message-type values. | Keep in Review; reopen from B12 automation smoke or fresh reference evidence for SQL-relation based message filtering beyond the triggering message, additional processed-content sources, or exact cross-group query precedence. | Focused automation condition validator/filter/listener tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. | | 0 | P3.74 contact type default parity | Implemented for direct create and CSV import paths: blank GoChat contacts now default to Chatwoot `visitor`, and visitor contacts with email, phone, or social details upgrade to `lead`. | Keep in Review; reopen from B12 CRM/import smoke or fresh reference evidence for channel-specific social detail keys or bulk-import merge edge cases. | Focused contact model/import tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. | | 0 | P3.73 conversation filter permission parity | Implemented for reused dashboard advanced filters: `POST /conversations/filter` now scopes results and meta counts through Chatwoot-style inbox visibility before applying payload/status/label filters. Administrators see all account conversations; agents see only assigned inbox conversations. | Keep in Review; reopen from B12 conversation filter smoke or fresh reference evidence for enterprise custom-role overrides beyond the current non-SSO scope. | Focused conversation service filter permission tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. | @@ -239,6 +240,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.76 conversation filter query message-type parity | `internal/service/conversation_service.go`, `internal/service/conversation_service_test.go` | `reference/chatwoot/app/finders/conversation_finder.rb#filter_by_query`, `reference/chatwoot/app/models/message.rb`, `internal/model/enums.go`, `internal/repository/conversation_repo.go` | Conversation service `Filter` query search now follows the same incoming/outgoing restriction as Chatwoot `ConversationFinder#filter_by_query`, but uses GoChat's persisted message type strings instead of raw Chatwoot enum integers. The query also uses a dialect-aware content predicate so SQLite focused tests exercise the same matching path while PostgreSQL keeps `ILIKE`. | Review by `feat(conversations): align filter query message types`; focused service test proves `q` matches incoming message content, excludes activity-only conversations, and ignores non-matching content; full `go test ./...` and `git diff --check` must pass. No route artifacts change. | | P3.75 automation message filter parity | `internal/automation/condition_filter.go`, `internal/automation/condition_validator.go`, `internal/automation/listener.go`, `internal/automation/model.go`, `internal/automation/condition_filter_test.go`, `internal/automation/condition_validator_test.go`, `internal/automation/listener_test.go` | `reference/chatwoot/app/services/automation_rules/conditions_filter_service.rb`, `reference/chatwoot/app/services/automation_rules/condition_validation_service.rb`, `reference/chatwoot/lib/filters/filter_keys.yml`, `reference/chatwoot/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js`, `reference/chatwoot/app/javascript/dashboard/helper/automationHelper.js` | Automation `message_created` conditions now evaluate the triggering message payload like the inspected reference message filters: `content` uses `processed_message_content` from content attributes when available and falls back to `messages.content`; `private_note` maps to `messages.private`; `message_type` accepts Chatwoot enum integers (`incoming=0`, `outgoing=1`, `activity=2`, `template=3`) and local strings. Validation now restricts message filter operators to the Chatwoot filter key contract. | Review by `feat(automation): align message filters`; focused matcher tests cover content/private/message-type matches, non-matches, missing message data, and OR message groups; validator tests cover supported/unsupported message operators; listener test proves `message_created` rules execute or skip from message filters. Full `go test ./...` and `git diff --check` must pass. No route artifacts change. | | P3.74 contact type default parity | `internal/model/contact.go`, `internal/model/model_test.go`, `internal/service/contact_service_g3_test.go` | `reference/chatwoot/app/models/contact.rb`, `reference/chatwoot/app/services/contacts/sync_attributes.rb`, `reference/chatwoot/spec/models/contact_spec.rb`, `reference/chatwoot/spec/services/contacts/sync_attributes_spec.rb`, `reference/chatwoot/app/services/data_import/contact_manager.rb` | Contact persistence now mirrors Chatwoot enum/default behavior before CRM scopes see imported records: blank `contact_type` is normalized to `visitor`, and `visitor` contacts with email, phone number, or social additional attributes become `lead`. CSV imports without a `contact_type` column therefore create CRM-v2-visible leads when identity details are present and true visitors when they are absent. | Review by `feat(contacts): default contact type`; focused model/import tests assert default visitor, detail-based lead upgrade, and CSV import defaults; full `go test ./...` and `git diff --check`. No route artifacts change. | | P3.73 conversation filter permission parity | `internal/service/conversation_service.go`, `internal/service/conversation_service_test.go` | `reference/chatwoot/app/services/conversations/filter_service.rb`, `reference/chatwoot/app/services/conversations/permission_filter_service.rb`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb#filter` | Advanced conversation filters now apply the Chatwoot permission base relation before payload/status/label filters. Non-admin agents are restricted to conversations whose inbox is in their account-scoped assigned inboxes; administrators keep full account visibility; missing account memberships return an empty result set. | Review by `feat(conversations): scope filter permissions`; focused service tests assert agent filtering hides unassigned-inbox matches while administrators still see all matching inboxes; full `go test ./...` and `git diff --check`. No route artifacts change. | @@ -2685,3 +2687,4 @@ Verification milestone gates: - 2026-06-07: P3.74 contact type default checkpoint prepared as `feat(contacts): default contact type`; audited Chatwoot contact enum defaults, `Contacts::SyncAttributes`, model specs, sync specs, and CSV import behavior. GoChat contacts now default blank `contact_type` to `visitor` and upgrade visitor contacts with email, phone, or social details to `lead`, closing the import/CRM-v2 drift where CSV-created contacts without an explicit type could stay empty. Focused contact model/import tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change. - 2026-06-07: Documentation checkpoint prepared as `docs: finalize parity tracking plan`; confirms `792786d feat(contacts): default contact type` is already committed, keeps `docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md` as the single active tracker, and lands P3.75 automation message filter parity as the next executable implementation slice. The row records inspected Chatwoot automation filter/validation/frontend files, Go owner files, message attribute contracts, and close gates. Verification for this docs-only checkpoint: `git diff --check`. - 2026-06-07: P3.75 automation message filter checkpoint prepared as `feat(automation): align message filters`; audited Chatwoot automation `ConditionsFilterService`, `ConditionValidationService`, `filter_keys.yml`, and reused automation builder constants/helper. GoChat `message_created` automation rules now evaluate `message_type`, `private_note`, and `content` from the triggering message payload, including Chatwoot numeric message type values, processed-content fallback, boolean private-note matching, content equality/containment operators, and validator rejection for unsupported message operators. Focused automation matcher/validator/listener tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change. +- 2026-06-07: P3.76 conversation filter query message-type checkpoint prepared as `feat(conversations): align filter query message types`; audited Chatwoot `ConversationFinder#filter_by_query`, message enum behavior, and GoChat persisted message type storage. GoChat conversation filter `q` searches now keep the Chatwoot incoming/outgoing-only restriction while comparing against local string message types instead of numeric enum values, so activity/template messages remain excluded and valid incoming/outgoing content is found. Focused conversation service query-filter test passed; full `go test ./...` and `git diff --check` passed. No route artifacts change. diff --git a/internal/service/conversation_service.go b/internal/service/conversation_service.go index 1214c991..23e3d97f 100644 --- a/internal/service/conversation_service.go +++ b/internal/service/conversation_service.go @@ -836,10 +836,10 @@ func (s *ConversationService) Filter(ctx context.Context, accountID uint, userID // 9. Filter by query (q) — 1:1 Chatwoot: filter_by_query // Search messages.content ILIKE, restricted to incoming/outgoing message types if params.Query != "" { - searchTerm := "%" + params.Query + "%" + searchClause, searchTerm := conversationMessageSearchClause(query, params.Query) query = query.Joins("JOIN messages ON messages.conversation_id = conversations.id"). - Where("messages.content ILIKE ?", searchTerm). - Where("messages.message_type IN ?", []int{0, 1}) // incoming=0, outgoing=1 + Where(searchClause, searchTerm). + Where("messages.message_type IN ?", []string{string(model.MessageTypeIncoming), string(model.MessageTypeOutgoing)}) } // 10. Filter by source_id — 1:1 Chatwoot: filter_by_source_id @@ -898,6 +898,14 @@ func (s *ConversationService) Filter(ctx context.Context, accountID uint, userID }, nil } +func conversationMessageSearchClause(query *gorm.DB, value string) (string, string) { + term := "%" + value + "%" + if query != nil && query.Dialector != nil && query.Dialector.Name() != "postgres" { + return "LOWER(messages.content) LIKE ?", "%" + strings.ToLower(value) + "%" + } + return "messages.content ILIKE ?", term +} + func (s *ConversationService) applyConversationPermissionFilter(ctx context.Context, accountID, userID uint, query *gorm.DB) (*gorm.DB, error) { if userID == 0 { return query, nil diff --git a/internal/service/conversation_service_test.go b/internal/service/conversation_service_test.go index b428ac3b..2e064905 100644 --- a/internal/service/conversation_service_test.go +++ b/internal/service/conversation_service_test.go @@ -378,6 +378,31 @@ func TestConversationService_Filter_AllowsAdministratorAllInboxes(t *testing.T) assert.Equal(t, int64(2), result.Count.AllCount) } +func TestConversationService_Filter_QueryUsesStoredMessageTypeStrings(t *testing.T) { + svc, db := setupConversationService(t) + + account := createConversationServiceTestAccount(t, db) + inbox := createConversationServiceTestInbox(t, db, account.ID) + contact := createConversationServiceTestContact(t, db, account.ID) + admin := &model.User{Name: "Filter Query Admin", Email: "filter-query-admin@example.com"} + require.NoError(t, db.Create(admin).Error) + require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: admin.ID, Role: "administrator"}).Error) + + match := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open") + activityOnly := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open") + noMatch := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open") + + require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: match.ID, Content: "Needle from customer", MessageType: string(model.MessageTypeIncoming)}).Error) + require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: activityOnly.ID, Content: "Needle activity", MessageType: string(model.MessageTypeActivity)}).Error) + require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: noMatch.ID, Content: "other", MessageType: string(model.MessageTypeIncoming)}).Error) + + result, err := svc.Filter(context.Background(), account.ID, admin.ID, FilterParams{Query: "needle", AssigneeType: "all"}, 0, 25) + require.NoError(t, err) + require.Len(t, result.Conversations, 1) + assert.Equal(t, match.ID, result.Conversations[0].ID) + assert.Equal(t, int64(1), result.Count.AllCount) +} + // ========== MarkUnread Tests ========== func TestConversationService_MarkUnread(t *testing.T) {