feat(conversations): align filter query message types

This commit is contained in:
2026-06-07 06:53:41 +08:00
parent 5efe61f801
commit bd20e6d80f
3 changed files with 43 additions and 7 deletions
+7 -4
View File
@@ -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.
+11 -3
View File
@@ -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
@@ -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) {