feat(conversations): align transcript responses
This commit is contained in:
@@ -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 `171cbd1 feat(labels): align chatwoot label payloads`, with this implementation checkpoint prepared as `feat(custom-fields): align chatwoot saved view payloads`.
|
||||
- Latest implementation checkpoint: this checkpoint, prepared as `feat(custom-fields): align chatwoot saved view payloads`.
|
||||
- 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`.
|
||||
- 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: P3.6 custom filters and custom attribute definitions now align the reused Chatwoot saved views/settings attributes frontend contract: `/custom_filters` list returns a raw array scoped by current user and `filter_type` with default `conversation`, create/update accept raw frontend bodies and nested Rails-style wrappers, mutation responses are raw model partials with Chatwoot fields only, and create/update status is `200 OK`; `/custom_attribute_definitions` list/show/create/update return raw Jbuilder-compatible payloads, accept raw frontend enum integers and string values, normalize stored `attribute_model` to Chatwoot `*_attribute` values, and keep delete as `204 No Content`. P3.6 label CRUD already aligns Chatwoot account `/labels` list/create/show/update/delete payloads, `title/description/color/show_on_sidebar` persistence, nested frontend request bodies, legacy `/tags` compatibility, account-scoped lookup, empty `200 OK` delete, and label association cleanup/rename side effects. B11.1a aligns Captain assistant CRUD/tools/inbox bindings; B11.1b aligns Captain scenarios and custom tools; B11.1c aligns Captain documents, assistant responses, bulk actions, and custom-tool test payloads; B11.2 aligns Copilot thread/message create/list/get/delete payloads, account/user scoping, and no-LLM fallback persistence; B11.3a aligns Captain preferences show/update payloads and account-level model/feature storage; B11.3b aligns Captain playground request/response payloads, account scoping, and no-LLM fallback; B11.3c adds the fakeable Captain document sync backend gate with disabled, failed, and fake-success states; B11.3d aligns Captain task request/response payloads, no-provider disabled states, follow-up context, suggestion persistence, and Copilot message tool-call key validation; B11.3e aligns Captain stream DTOs/disabled SSE fallbacks and Copilot push-event payload shapes; 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 continued Phase 6 placeholder audit from fresh reference/smoke evidence.
|
||||
- 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.
|
||||
- `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`.
|
||||
@@ -88,7 +88,7 @@ This table is the shortest authoritative handoff view. If an older lower section
|
||||
| Priority | Workstream | Current state | Next checkpoint | Commit close rule |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | Phase 2/3 drift | Tracked route parity is 0 missing for the current critical set; notification list/action serializers, Devise password reset/confirmation payloads, CRM shared attachment payloads, account/settings payloads, assignable-agent payloads, label CRUD payloads, custom filters, and custom attribute definitions now match the inspected Chatwoot frontend contract. | Continue Phase 6 placeholder audit, then open the next named serializer/controller drift slice from reference/smoke evidence. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. |
|
||||
| 2 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; account/contact/conversation/message/inbox audit remains broad. | Continue fresh placeholder audit and assign every frontend-reachable stub to a tracked owner. | `rg` placeholder audit is recorded and no reused-frontend blocker is ownerless. |
|
||||
| 2 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; fresh `rg` audit shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript response drift is closed. | Continue targeted account/contact/conversation/message/inbox drift from reference/smoke evidence. | `rg` placeholder audit is recorded and no reused-frontend blocker is ownerless. |
|
||||
| 3 | B12 optional live smoke | API/browser/enterprise smoke commands are checked in; live runs need PostgreSQL, Redis, Meilisearch, Vite, and Chrome. | Run full live smoke when environment is available and map failures to the board. | `docs/parity/frontend_smoke_report.md` records pass/fail and linked owners. |
|
||||
| 4 | B9.3 delayed automation actions | Current reference exposes no delayed automation action params; scheduled-item work is already P5.12; `send_email_to_team` is durable and `add_sla` mutates conversation/applied SLA state. | Keep automation drift closed if future reference/smoke exposes delayed params or unsupported action shapes. | Automation worker/action fixtures verify queued team email replay, retry visibility through worker jobs, and SLA action idempotency. |
|
||||
| 5 | P5.13 reports/analytics | P5.13a derives visible report aggregates from persisted rows; P5.13b adds lazy rollup freshness, durable day rollup jobs, and `/reports` metric timeseries. | Keep report drift closed as frontend smoke or reference inspection exposes additional metrics. | Report fixtures verify timeseries values, cache/freshness behavior, and no hidden placeholder JSON. |
|
||||
@@ -106,6 +106,7 @@ These rows are the executable development plan from this point forward. A checkp
|
||||
| P5.13b scheduled/cached analytics | `internal/service/analytics_service.go`, `internal/service/analytics_query_helpers.go`, `internal/service/reporting_rollup_worker.go`, `internal/service/reporting_rollup_service.go`, report handlers/services, worker bootstrap | Chatwoot report controllers/services used by dashboard analytics, `Reports::DataSource`, reporting rollup/backfill jobs | Wire scheduled or lazy cached rollup freshness/idempotency and route `GET /reports` to metric timeseries instead of the summary handler. Define freshness rules for expensive rollups. | Review by `feat(reports): add analytics timeseries rollups`; report fixtures prove timeseries values are derived from persisted conversations/messages/reporting events, rollups refresh idempotently, durable `reporting:rollup_day` jobs replay, and hidden placeholder report JSON does not reappear. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
@@ -141,6 +142,7 @@ This ledger records the committed parity checkpoints that future slices should b
|
||||
|
||||
| Commit | Scope | Verification summary | Follow-up state |
|
||||
| --- | --- | --- | --- |
|
||||
| `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. |
|
||||
| `docs: refresh parity tracker baseline` | Documentation-only checkpoint after `2ab7b58 feat(accounts): align account payloads`. Refreshes the Current Baseline to the committed account payload work, records the clean handoff state, and lands the next executable P3.6 label CRUD parity contract from direct Chatwoot reference/frontend inspection. | `git diff --check`. No Go code changed. | Start `feat(labels): align chatwoot label payloads`, then continue Phase 6 placeholder audit. |
|
||||
|
||||
@@ -687,12 +687,16 @@ func (h *ConversationHandler) Transcript(c *gin.Context) {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Email) == "" {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "email param missing"})
|
||||
return
|
||||
}
|
||||
|
||||
conversation, ok := h.resolveConversationRoute(c, accountID, conversationID)
|
||||
if !ok {
|
||||
@@ -703,7 +707,7 @@ func (h *ConversationHandler) Transcript(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, nil)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// UpdateCustomAttributes updates the custom attributes of a conversation.
|
||||
|
||||
@@ -255,14 +255,7 @@ func (s *ConversationHandlerTestSuite) TestTranscript_Success() {
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
|
||||
var resp struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.True(s.T(), resp.Success)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestTranscript_InvalidAccountID() {
|
||||
@@ -286,8 +279,11 @@ func (s *ConversationHandlerTestSuite) TestTranscript_MissingEmail() {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// ShouldBindJSON fails when required field is missing
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), "email param missing", resp["error"])
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestTranscript_InvalidEmail() {
|
||||
@@ -299,8 +295,8 @@ func (s *ConversationHandlerTestSuite) TestTranscript_InvalidEmail() {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// email validation fails via binding:"required,email"
|
||||
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
||||
assert.Equal(s.T(), http.StatusOK, w.Code)
|
||||
assert.Empty(s.T(), w.Body.String())
|
||||
}
|
||||
|
||||
func (s *ConversationHandlerTestSuite) TestTranscript_ConversationNotFound() {
|
||||
|
||||
Reference in New Issue
Block a user