feat(conversations): align custom attribute response

This commit is contained in:
2026-06-06 01:07:42 +08:00
parent 1fa46faa45
commit 7425e269d7
3 changed files with 11 additions and 6 deletions
+5 -3
View File
@@ -16,10 +16,10 @@ Build GoChat as a Go backend that can directly reuse the frontend from `referenc
## Current Baseline
- Current tracking checkpoint: 2026-06-06 after `328d16c feat(custom-fields): align chatwoot saved view payloads`, with this implementation checkpoint prepared as `feat(conversations): align transcript responses`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align transcript responses`.
- Current tracking checkpoint: 2026-06-06 after `1fa46fa feat(conversations): align transcript responses`, with this implementation checkpoint prepared as `feat(conversations): align custom attribute response`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align custom attribute response`.
- Latest documentation-only checkpoint: `8b378c7 docs: refresh parity tracker baseline`; this document is now the active follow-up plan and supersedes `.hermes/plans/*`.
- Worktree status at this implementation checkpoint: Phase 6 conversation transcript response drift is closed for the reused dashboard route: `POST /conversations/:id/transcript` now returns empty `200 OK`, accepts any nonblank `email` value like Chatwoot, and returns `422 { error: "email param missing" }` for missing email instead of local validation envelopes. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters and custom attribute definitions already align the reused Chatwoot saved views/settings attributes frontend contract. P3.6 label CRUD already aligns Chatwoot account `/labels` CRUD. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is the next named Phase 2/3 or Phase 6 drift from fresh reference/smoke evidence.
- Worktree status at this implementation checkpoint: Phase 6 conversation custom attribute response drift is closed for the reused dashboard route: `POST /conversations/:id/custom_attributes` now returns Chatwoot `{ custom_attributes }` instead of the full local conversation serializer. The preceding transcript slice closed `POST /conversations/:id/transcript` response/status drift. The fresh placeholder audit found remaining `chatwootParityStub` routes only in webhook handler-not-injected fallbacks, not in the reused dashboard account/contact/conversation/message/inbox critical path. P3.6 custom filters/custom attribute definitions and label CRUD are already in Review. B11.1a-B11.3e Captain/Copilot slices remain in Review; B12.1/B12.2/B12.3 smoke harnesses remain in Review; P5 durable job work through P5.13b is in Review. Next active implementation slice is the next named Phase 2/3 or Phase 6 drift from fresh reference/smoke evidence.
- `go test ./...` passes.
- Route dump succeeds with `TOTAL: 837` after adding Chatwoot auth reset/confirmation routes and `GET /api/v1/accounts/:account_id/contacts/:contact_id/attachments`.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
@@ -107,6 +107,7 @@ These rows are the executable development plan from this point forward. A checkp
| P3.6 label CRUD payload parity | `internal/handler/api/v1/label_handler.go`, `internal/service/tag_service.go`, `internal/model/tag.go`, `internal/repository/tag_repo.go`, migrations, label handler/service tests | `reference/chatwoot/app/controllers/api/v1/accounts/labels_controller.rb`, `app/views/api/v1/accounts/labels/*.json.jbuilder`, `app/models/label.rb`, `db/schema.rb`, dashboard `api/labels.js`, `store/modules/labels.js` | Account `/labels` CRUD now returns Chatwoot `{ payload: [...] }` lists, raw mutation/show payloads, empty `200 OK` delete, `title/description/color/show_on_sidebar` persistence, title lowercasing/uniqueness, account scope, nested `{ label: ... }` request bodies, legacy `name` compatibility, default color, and local association cleanup/rename side effects. | Review by `feat(labels): align chatwoot label payloads`; focused handler/service/repository tests, combined handler/service/router tests, `go test ./cmd/migrate -count=1`, full `go test ./...`, and `git diff --check` passed. |
| P3.6 custom filters/custom attribute definitions payload parity | `internal/handler/api/v1/custom_filter_handler.go`, `internal/service/custom_filter_service.go`, `internal/repository/custom_filter_repo.go`, `internal/handler/api/v1/custom_attribute_definition_handler.go`, `internal/service/custom_attribute_definition_service.go`, `internal/service/custom_attribute_value_service.go`, handler/service/repository tests | `reference/chatwoot/app/controllers/api/v1/accounts/custom_filters_controller.rb`, `app/views/api/v1/accounts/custom_filters/*.json.jbuilder`, `app/views/api/v1/models/_custom_filter.json.jbuilder`, dashboard `api/customViews.js`, `store/modules/customViews.js`, `custom_attribute_definitions_controller.rb`, `_custom_attribute_definition.json.jbuilder`, dashboard `api/attributes.js`, `store/modules/attributes.js`, settings attributes screens/constants | `/custom_filters` and `/custom_attribute_definitions` now return raw Chatwoot arrays/objects instead of local envelopes, accept raw reused-frontend bodies plus Rails-style wrappers, use `200 OK` mutation responses, preserve `204` deletes, scope saved views by current user and default `filter_type=conversation`, normalize custom attribute enum integers to Chatwoot string values, and keep custom attribute value validation compatible with normalized `*_attribute` definitions plus legacy rows. | Review by `feat(custom-fields): align chatwoot saved view payloads`; focused handler/service/repository tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` must pass. |
| P6 conversation transcript response parity | `internal/handler/api/v1/conversation_handler.go`, conversation handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb`, dashboard `api/inbox/conversation.js` | Account conversation transcript now follows Chatwoot's controller contract: missing email returns `422 { error: "email param missing" }`, nonblank email schedules through the existing service boundary and returns empty `200 OK`, and invalid-looking but nonblank email values are not rejected by local email format validation. | Review by `feat(conversations): align transcript responses`; focused transcript handler/service tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` must pass. |
| P6 conversation custom attributes response parity | `internal/handler/api/v1/conversation_handler.go`, conversation handler tests | `reference/chatwoot/app/controllers/api/v1/accounts/conversations_controller.rb`, `app/views/api/v1/accounts/conversations/custom_attributes.json.jbuilder`, dashboard `api/inbox/conversation.js`, conversation store action | Account conversation custom attribute updates now return Chatwoot `{ custom_attributes: ... }` only, matching the store action that reads `response.data.custom_attributes`, instead of returning the full conversation serializer with unrelated fields. | Review by `feat(conversations): align custom attribute response`; focused handler tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` must pass. |
| Phase 2/3 drift audit | `cmd/route_parity`, `docs/parity/*`, serializer tests | `reference/chatwoot/config/routes.rb`, controller Jbuilder views, reused frontend API clients | Convert any smoke/reference mismatch into a named route, controller, or serializer slice. Static route extraction remains acceptable until Ruby/Bundler is available. | Regenerated route parity shows 0 missing tracked frontend routes; new serializer fixtures cover the drift. |
| Phase 6 placeholder burn-down | Account/contact/conversation/message/inbox handlers and services | Matching reference controllers/Jbuilder views plus reused frontend screens | Re-run placeholder audit and assign every frontend-reachable stub to a specific owner. Burn down the highest-impact stubs before broad feature expansion. | `rg` placeholder audit is recorded here; no reused-frontend critical path is ownerless. |
| B12 live smoke | `scripts/parity_frontend_smoke.sh`, `docs/parity/frontend_smoke_report.md`, `cmd/gochat` | Reused `reference/chatwoot` Vite frontend, dashboard route/API clients | Run optional live API/browser/enterprise smoke with PostgreSQL, Redis, Meilisearch, GoChat, Vite, and Chrome. Convert failures into named rows above. | Smoke report records command, environment, pass/fail, artifacts, and linked follow-up owners. |
@@ -142,6 +143,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(conversations): align custom attribute response` | Advances Phase 6 dashboard conversation drift by aligning `POST /api/v1/accounts/:account_id/conversations/:conversation_id/custom_attributes` with Chatwoot `custom_attributes.json.jbuilder` and the reused conversation store action. The endpoint still persists the submitted custom attributes, but the response is now only `{ custom_attributes: ... }` and no longer leaks the full local conversation serializer, `id`, or local envelope fields. | `go test ./internal/handler/api/v1 -run 'TestConversationHandlerTestSuite/TestUpdateCustomAttributes' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. |
| `feat(conversations): align transcript responses` | Advances Phase 6 dashboard conversation drift by aligning `POST /api/v1/accounts/:account_id/conversations/:conversation_id/transcript` with Chatwoot `ConversationsController#transcript` and the reused dashboard conversation API. Transcript success now returns empty `200 OK` instead of the local `{ success, data }` envelope; missing `email` returns Chatwoot `422 { error: "email param missing" }`; local email-format validation no longer rejects nonblank values that Chatwoot would pass through to the mailer. Fresh placeholder audit found only webhook handler nil-fallback `chatwootParityStub` routes, with no account/contact/conversation/message/inbox dashboard-critical stub owner left by this slice. | `go test ./internal/handler/api/v1 -run 'TestConversationHandlerTestSuite/TestTranscript' -count=1`; `go test ./internal/service -run ConversationService_SendTranscript -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | Continue the next named Phase 2/3 or Phase 6 drift from reference/smoke evidence. |
| `feat(custom-fields): align chatwoot saved view payloads` | Advances P3.6 serializer/controller parity for saved views and settings attributes. Account `/custom_filters` now matches Chatwoot `CustomFiltersController`, `_custom_filter.json.jbuilder`, and dashboard `customViews.js`/store behavior: list returns a raw array, defaults to `filter_type=conversation`, scopes by current user, accepts raw frontend bodies plus nested `{ custom_filter }`, returns raw mutation payloads with no `account_id/user_id`, and uses `200 OK` for create/update plus `204` delete. Account `/custom_attribute_definitions` now matches `CustomAttributeDefinitionsController`, `_custom_attribute_definition.json.jbuilder`, dashboard `attributes.js`/store, and settings attribute enum constants: list/show/create/update return raw arrays/objects, create/update accept raw integer enum bodies plus wrappers, stored/serialized `attribute_model` values normalize to Chatwoot `conversation_attribute/contact_attribute/company_attribute`, and custom attribute value validation can find normalized and legacy definition rows. | `go test ./internal/handler/api/v1 -run 'CustomFilter\|CustomAttributeDefinition' -count=1`; `go test ./internal/service -run 'CustomFilter\|CustomAttributeDefinition' -count=1`; `go test ./internal/repository -run 'CustomFilter\|CustomAttributeDefinition' -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./...`; `git diff --check`. | P3.6 custom filters/custom attribute definitions move to Review; continue Phase 6 placeholder audit from fresh reference/smoke evidence. |
| `feat(labels): align chatwoot label payloads` | Advances P3.6 label CRUD parity by aligning account `/labels` with Chatwoot `LabelsController`, label Jbuilder views, `Label` model/schema, and dashboard labels API/store. List now returns `{ payload }`; create/show/update return raw label payloads with `id`, `title`, `description`, `color`, and `show_on_sidebar`; create/update accept nested `{ label }` bodies and legacy raw `name`; delete returns empty `200 OK`; title normalization, default color, account scope, persisted description/sidebar flags, and local conversation/contact association cleanup are covered. | `go test ./internal/handler/api/v1 -run LabelHandler -count=1`; `go test ./internal/service -run 'TagService|LabelService' -count=1`; `go test ./internal/repository -run TagRepo -count=1`; `go test ./internal/handler/api/v1 ./internal/service ./internal/router -count=1`; `go test ./cmd/migrate -count=1`; `go test ./...`; `git diff --check`. | P3.6 label CRUD moves to Review; continue Phase 6 placeholder audit, then custom attributes/custom filters drift. |
@@ -744,7 +744,7 @@ func (h *ConversationHandler) UpdateCustomAttributes(c *gin.Context) {
return
}
c.JSON(http.StatusOK, serializeConversation(c.Request.Context(), h.conversationSvc.DB(), conversation))
c.JSON(http.StatusOK, gin.H{"custom_attributes": conversation.CustomAttributes})
}
// ListAttachments returns paginated message attachments for a conversation.
@@ -327,13 +327,16 @@ func (s *ConversationHandlerTestSuite) TestUpdateCustomAttributes_Success() {
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp struct {
ID uint `json:"id"`
CustomAttributes map[string]interface{} `json:"custom_attributes"`
}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), s.testConv.ID, resp.ID)
assert.Equal(s.T(), "vip_customer", resp.CustomAttributes["priority_reason"])
var rawResp map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &rawResp)
assert.NoError(s.T(), err)
assert.NotContains(s.T(), rawResp, "id")
assert.NotContains(s.T(), rawResp, "success")
}
func (s *ConversationHandlerTestSuite) TestUpdateCustomAttributes_InvalidAccountID() {