feat(accounts): serialize enabled features

This commit is contained in:
2026-06-07 06:12:04 +08:00
parent 46eb7aafbd
commit ee9477ae58
3 changed files with 115 additions and 10 deletions
+9 -5
View File
@@ -49,12 +49,12 @@ Hermes task landing checklist:
## Current Baseline
- Current tracking checkpoint: 2026-06-07 P3.71 contact referer filter parity, prepared as `feat(contacts): filter by referer`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(contacts): filter by referer`.
- Latest documentation/tooling checkpoint: this tracker update records reused contact filter frontend `referer` support. No route artifacts change.
- Current tracking checkpoint: 2026-06-07 P3.72 account enabled-features serialization parity, prepared as `feat(accounts): serialize enabled features`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(accounts): serialize enabled features`.
- Latest documentation/tooling checkpoint: this tracker update records Chatwoot `enabled_features` account payload parity. 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: contact advanced filter now accepts the reused frontend `referer` attribute and matches it against `contacts.additional_attributes.referer` with equality and containment operators, closing the known frontend/reference drift for CRM filters. Existing P5.8c contact import search indexing, P3.70 global contact search resolved-scope parity, P3.69 contact index/filter/export resolved-scope parity, P3.68 contact filter payload parity, P3.64-P3.67 conversation filter payload parity, P3.63 search message-order parity, P3.62 trimmed search payload fields, P3.61 sender-name, P3.60 numeric message-type 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 message filters, conversation permission-filter depth, CRM v2 feature-flag serialization, import paths creating unexpected contact types, 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.
- Worktree status at this implementation checkpoint: account serialization now returns Chatwoot-style `enabled_features`: default enabled `config/features.yml` flags are present for empty GoChat accounts, explicit true flags such as `crm_v2` are included, and false flags are omitted so reused frontend feature gates behave like `Account#enabled_features`. Existing P3.71 contact referer filter parity, P5.8c contact import search indexing, P3.70 global contact search resolved-scope parity, P3.69 contact index/filter/export resolved-scope parity, P3.68 contact filter payload parity, P3.64-P3.67 conversation filter payload parity, P3.63 search message-order parity, P3.62 trimmed search payload fields, P3.61 sender-name, P3.60 numeric message-type 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 message filters, conversation permission-filter depth, import paths creating unexpected contact types, the next reused-frontend mismatch, or 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.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
@@ -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.72 account enabled-features serialization parity | Implemented for reused frontend account feature gates: GoChat account payloads now serialize `features` like Chatwoot `Account#enabled_features`, seeding default enabled flags from `config/features.yml`, including explicit true flags such as `crm_v2`, and omitting false flags. | Keep in Review; reopen from B12 account/CRM smoke or fresh reference evidence for installation-config default feature drift, cloud-only feature filtering, or bitfield migration needs. | Focused account handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
| 0 | P3.71 contact referer filter parity | Implemented for reused CRM contact advanced filters: GoChat now accepts frontend `referer` payload conditions and filters `contacts.additional_attributes.referer` with equality and containment operators. | Keep in Review; reopen from B12 CRM filter smoke or fresh reference/frontend evidence for exact referer location drift, link case-sensitivity expectations, or export filter reuse gaps. | Focused contact filter handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
| 0 | P5.8c contact import search indexing parity | Implemented for imported contact rows: CSV imports now call the contact search indexer after successful create and after successful duplicate merge/update, so Meilisearch documents stay current for email, phone number, identifier, and `contact_type` fields used by contact/global search. | Keep in Review; reopen from B12 CRM/search smoke or fresh reference evidence for label-aware contact search documents, failed-label partial updates, or durable import worker replay drift. | Focused contact import service tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
| 0 | P3.70 global contact search resolved-scope parity | Implemented for Chatwoot `SearchService#filter_contacts`: GoChat global search and `/search/contacts` now apply resolved-contact scoping to contact results. Meilisearch contact documents carry `contact_type` and `contact_has_details`, default scopes require email/phone/identifier, and `crm_v2` scopes require `contact_type = lead`. | Keep in Review; dashboard `contacts#search` intentionally remains unscoped to match `ContactsController#search`; reopen from B12 search smoke or fresh reference evidence for Meilisearch live index drift, CRM v2 feature-flag serialization, or missing contact reindex paths after contact-type/detail changes. | Focused search service, Meilisearch engine, SearchRepo, and search handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
@@ -235,6 +236,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.72 account enabled-features serialization parity | `internal/handler/api/v1/account_handler.go`, `internal/handler/api/v1/account_handler_test.go` | `reference/chatwoot/app/models/concerns/featurable.rb`, `reference/chatwoot/app/views/api/v1/models/_account.json.jbuilder`, `reference/chatwoot/config/features.yml`, `reference/chatwoot/app/javascript/dashboard/store/modules/accounts.js` | Account show/list payloads now serialize `features` as Chatwoot `enabled_features` instead of raw GoChat `feature_flags`: default enabled flags are present for empty accounts, explicit true flags like `crm_v2` are included, and false flags are omitted so reused frontend feature gates see the same truthy-key contract. | Review by `feat(accounts): serialize enabled features`; focused handler tests assert default enabled flags, explicit `crm_v2`, and false-flag omission; full `go test ./...` and `git diff --check`. No route artifacts change. |
| P3.71 contact referer filter parity | `internal/repository/contact_repo.go`, `internal/handler/api/v1/contact_handler_crud_test.go` | `reference/chatwoot/app/javascript/dashboard/components-next/filter/contactProvider.js`, `reference/chatwoot/app/javascript/dashboard/components-next/filter/helper/filterHelper.js`, `reference/chatwoot/app/services/contacts/filter_service.rb`, `reference/chatwoot/lib/filters/filter_keys.yml` | Contact advanced filters now accept the reused frontend's built-in `referer` contact filter and evaluate it against `contacts.additional_attributes.referer` with `equal_to`, `not_equal_to`, `contains`, and `does_not_contain`. This closes the prior frontend/reference drift where the current dashboard exposed `referer` even though the backend filter key list does not declare it under contacts. | Review by `feat(contacts): filter by referer`; focused handler tests assert `referer contains` matches only contacts whose additional attributes carry the referer URL; full `go test ./...` and `git diff --check`. No route artifacts change. |
| P5.8c contact import search indexing parity | `internal/service/contact_service.go`, `internal/service/contact_service_g3_test.go` | `reference/chatwoot/app/controllers/api/v1/accounts/contacts_controller.rb#import`, `reference/chatwoot/app/jobs/data_import_job.rb`, `reference/chatwoot/app/services/data_import/contact_manager.rb`, P3.70 Meilisearch contact document contract | Imported contacts now refresh the contact search index after successful CSV-created rows and after successful duplicate merge/update rows. This keeps Meilisearch contact documents current for identity fields and `contact_type`, which are used by global contact search resolved-scope filters. | Review by `feat(contacts): index imported contacts`; focused import tests assert imported create and merge paths call `IndexContact`; full `go test ./...` and `git diff --check`. No route artifacts change. |
| P3.70 global contact search resolved-scope parity | `internal/search/search_service.go`, `internal/search/engine.go`, `internal/search/engine_meili.go`, `internal/repository/search_repo.go`, `internal/handler/api/v1/search_handler.go`, search/repository tests | `reference/chatwoot/app/services/search_service.rb#filter_contacts`, `reference/chatwoot/app/models/contact.rb#resolved_contacts`, `reference/chatwoot/spec/models/contact_spec.rb`, `reference/chatwoot/app/controllers/api/v1/accounts/contacts_controller.rb#search` | Global search contact branches now use Chatwoot `SearchService#filter_contacts` resolved-contact base relation. DB fallback and Meilisearch filters both exclude anonymous contacts without email, phone number, or identifier unless `crm_v2` is enabled; `crm_v2` global contact search keeps only `contact_type = lead`. Meilisearch contact documents expose `contact_type` plus `contact_has_details` as filterable attributes. The dashboard `/contacts/search` path stays unscoped because the reference contacts controller searches all account contacts directly before pagination. | Review by `feat(search): align contact resolved scope`; focused tests assert SearchService resolved-scope filter injection, Meilisearch default and CRM v2 filter expressions, contact document resolved fields, and SearchRepo default/CRM v2 resolved scopes; full `go test ./...` and `git diff --check`. No route artifacts change. |
@@ -332,6 +334,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(accounts): serialize enabled features` | Advances P3.72 with Chatwoot account `enabled_features` serialization parity. GoChat account payloads now seed default enabled features from the local reference `config/features.yml`, include explicit true flags such as `crm_v2`, and omit false flags instead of exposing raw `feature_flags` to reused dashboard feature gates. | `go test ./internal/handler/api/v1 -run 'AccountHandlerSuite/TestGet_(FeaturesUseChatwootEnabledFeatures|DefaultFeaturesMatchChatwootDefaults)' -count=1`; full `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.72 to Review; continue Phase 2/3 drift audit for message filters, conversation permission-filter depth, import/contact-type drift, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(contacts): filter by referer` | Advances P3.71 with reused CRM contact filter parity for the frontend-exposed `referer` attribute. Contact advanced filters now evaluate `referer` against `contacts.additional_attributes.referer` with equality and containment operators instead of rejecting the current dashboard payload as an unknown attribute. | `go test ./internal/handler/api/v1 -run 'ContactHandlerCRUDTestSuite/TestFilter_ContactRefererFromFrontendProvider\|ContactHandlerCRUDTestSuite/TestFilter_ChatwootPayloadStandardAndAdditionalAttributes' -count=1`; full `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.71 to Review; continue Phase 2/3 drift audit for message filters, conversation permission-filter depth, CRM v2 feature-flag serialization, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(contacts): index imported contacts` | Advances P5.8c with contact import search-index parity. CSV import create and duplicate-merge/update paths now call the contact search indexer after the row and labels are successfully persisted, keeping Meilisearch contact documents current for identity fields and `contact_type` changes used by P3.70 resolved-scope search. | `go test ./internal/service -run 'TestContactService_ImportCSV_(ImportsValidRows\|MergesDuplicateEmail)' -count=1`; full `go test ./...`; `git diff --check`. No route artifacts change. | Move P5.8c to Review; continue Phase 2/3 drift audit for message filters, conversation permission-filter depth, CRM v2 feature-flag serialization, label-aware contact search documents, Phase 6 placeholder audit, or B12 live smoke. |
| `feat(search): align contact resolved scope` | Advances P3.70 with Chatwoot global contact search resolved-scope parity. Global search and `/search/contacts` now apply `SearchService#filter_contacts` semantics: default contact results require email, phone number, or identifier, while `crm_v2` contact search keeps only `contact_type = lead`. Meilisearch contact documents now include filterable `contact_type` and `contact_has_details`; dashboard `contacts#search` remains unscoped to match the reference controller. | `go test ./internal/search ./internal/repository ./internal/handler/api/v1 -run 'GlobalSearch_Applies\|MeiliSearchEngine_SearchSends.*Contact\|ContactDocumentSetsResolved\|SearchRepo_SearchContacts_Resolved\|SearchContacts' -count=1`; full `go test ./...`; `git diff --check`. No route artifacts change. | Move P3.70 to Review; continue Phase 2/3 drift audit for message filters, conversation permission-filter depth, CRM v2 feature-flag serialization, import/contact-type drift, Phase 6 placeholder audit, or B12 live smoke. |
@@ -2669,3 +2672,4 @@ Verification milestone gates:
- 2026-06-07: P3.70 global contact search resolved-scope checkpoint prepared as `feat(search): align contact resolved scope`; audited Chatwoot `SearchService#filter_contacts`, `Contact.resolved_contacts`, contact model specs, and `ContactsController#search`. GoChat global search and `/search/contacts` now apply resolved-contact scope while dashboard `/contacts/search` remains unscoped like the reference controller; Meilisearch contact docs now include `contact_type` and `contact_has_details` for default and CRM v2 filters. Focused search service, Meilisearch engine, SearchRepo, and handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
- 2026-06-07: P5.8c contact import search indexing checkpoint prepared as `feat(contacts): index imported contacts`; audited the contact import worker path and the P3.70 Meilisearch contact document contract. GoChat CSV import now reindexes contacts after successful imported creates and duplicate merges, keeping search documents current for identity fields and `contact_type`. Focused import service tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
- 2026-06-07: P3.71 contact referer filter checkpoint prepared as `feat(contacts): filter by referer`; audited reused `contactProvider.js`, `filterHelper.js`, `Contacts::FilterService`, and `filter_keys.yml`. GoChat contact advanced filters now accept the frontend-exposed `referer` attribute and match it against `contacts.additional_attributes.referer`, so CRM filter payloads using `contains` no longer fail as unknown attributes. Focused contact filter tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
- 2026-06-07: P3.72 account enabled-features serialization checkpoint prepared as `feat(accounts): serialize enabled features`; audited Chatwoot `Featurable#enabled_features`, account model serializer, `config/features.yml`, and reused dashboard account feature getter. GoChat account payloads now return Chatwoot-style truthy `features`: default enabled flags are present for empty accounts, explicit true flags such as `crm_v2` are included, and false flags are omitted. Focused account handler tests passed; full `go test ./...` and `git diff --check` passed. No route artifacts change.
+66 -5
View File
@@ -461,13 +461,74 @@ func serializeAccountSettings(account *model.Account) map[string]any {
return settings
}
func parseAccountFeatures(raw string) map[string]any {
features := map[string]any{}
var chatwootDefaultEnabledAccountFeatures = []string{
"inbound_emails",
"channel_email",
"channel_facebook",
"help_center",
"agent_bots",
"macros",
"agent_management",
"team_management",
"inbox_management",
"labels",
"custom_attributes",
"automations",
"canned_responses",
"integrations",
"voice_recorder",
"channel_website",
"campaigns",
"reports",
"crm",
"auto_resolve_conversations",
"chatwoot_v4",
"contact_chatwoot_support_team",
"channel_instagram",
"channel_tiktok",
"assignment_v2",
"captain_tasks",
}
func parseAccountFeatures(raw string) map[string]bool {
features := defaultEnabledAccountFeatures()
if raw == "" {
return features
return onlyEnabledAccountFeatures(features)
}
if err := json.Unmarshal([]byte(raw), &features); err != nil {
return features
objectFlags := map[string]bool{}
if err := json.Unmarshal([]byte(raw), &objectFlags); err == nil {
for key, enabled := range objectFlags {
features[key] = enabled
}
return onlyEnabledAccountFeatures(features)
}
arrayFlags := []string{}
if err := json.Unmarshal([]byte(raw), &arrayFlags); err == nil {
for _, key := range arrayFlags {
features[key] = true
}
return onlyEnabledAccountFeatures(features)
}
return onlyEnabledAccountFeatures(features)
}
func defaultEnabledAccountFeatures() map[string]bool {
features := make(map[string]bool, len(chatwootDefaultEnabledAccountFeatures))
for _, key := range chatwootDefaultEnabledAccountFeatures {
features[key] = true
}
return features
}
func onlyEnabledAccountFeatures(features map[string]bool) map[string]bool {
enabled := map[string]bool{}
for key, value := range features {
if value {
enabled[key] = true
}
}
return enabled
}
@@ -199,6 +199,46 @@ func (s *AccountHandlerTestSuite) TestGet_Success() {
assert.Contains(s.T(), resp, "cache_keys")
}
func (s *AccountHandlerTestSuite) TestGet_FeaturesUseChatwootEnabledFeatures() {
acc := &model.Account{
Name: "Feature Account",
Active: true,
Status: "active",
FeatureFlags: `{"crm_v2":true,"sla":false,"crm":false}`,
}
s.Require().NoError(s.db.Create(acc).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
features := resp["features"].(map[string]interface{})
assert.Equal(s.T(), true, features["crm_v2"])
assert.Equal(s.T(), true, features["channel_email"])
assert.Equal(s.T(), true, features["assignment_v2"])
assert.NotContains(s.T(), features, "sla")
assert.NotContains(s.T(), features, "crm")
}
func (s *AccountHandlerTestSuite) TestGet_DefaultFeaturesMatchChatwootDefaults() {
acc := s.seedAccount("Default Feature Account")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(acc.ID), 10), nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
resp := s.unmarshalResponse(w)
features := resp["features"].(map[string]interface{})
assert.Equal(s.T(), true, features["crm"])
assert.Equal(s.T(), true, features["macros"])
assert.Equal(s.T(), true, features["captain_tasks"])
assert.NotContains(s.T(), features, "crm_v2")
assert.NotContains(s.T(), features, "sla")
}
func (s *AccountHandlerTestSuite) TestGet_NotFound() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/99999", nil)