diff --git a/cmd/route_parity/main.go b/cmd/route_parity/main.go index 0bf16112..98b230cf 100644 --- a/cmd/route_parity/main.go +++ b/cmd/route_parity/main.go @@ -107,6 +107,7 @@ var criticalRoutes = []route{ {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/toggle_priority", Controller: "api/v1/accounts/conversations#toggle_priority", Source: "routes.rb:159"}, {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/transcript", Controller: "api/v1/accounts/conversations#transcript", Source: "routes.rb:157"}, {Method: "GET", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant", Controller: "api/v1/accounts/conversations#inbox_assistant", Source: "routes.rb:165"}, + {Method: "GET", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events", Controller: "api/v1/accounts/conversations#reporting_events", Source: "routes.rb:166"}, {Method: "GET", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/messages/", Controller: "api/v1/accounts/conversations/messages#index", Source: "routes.rb:142"}, {Method: "POST", Path: "/api/v1/accounts/:account_id/conversations/:conversation_id/messages/", Controller: "api/v1/accounts/conversations/messages#create", Source: "routes.rb:142"}, diff --git a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md index 555f4c75..abebe4ea 100644 --- a/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md +++ b/docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md @@ -49,15 +49,15 @@ Hermes task landing checklist: ## Current Baseline -- Current tracking checkpoint: 2026-06-06 conversation draft messages checkpoint, prepared as `feat(conversations): align draft message route`. -- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): align draft message route`. -- Latest documentation/tooling checkpoint: this tracker update for P3.20 plus the landed parity tracker history; this document is the active follow-up plan and supersedes `.hermes/plans/*`. +- Current tracking checkpoint: 2026-06-06 conversation reporting events checkpoint, prepared as `feat(conversations): expose reporting events`. +- Latest implementation checkpoint: this checkpoint, prepared as `feat(conversations): expose reporting events`. +- Latest documentation/tooling checkpoint: this tracker update for P3.21 plus the landed parity tracker history; this document is the active follow-up plan and supersedes `.hermes/plans/*`. - 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: conversation draft messages from `reference/chatwoot/config/routes.rb:152`, `Api::V1::Accounts::Conversations::DraftMessagesController`, and request specs are implemented for Chatwoot's singular conversation-scoped draft contract. `GET/PATCH/PUT/DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages` are now tracked and routed without a draft ID, resolve conversations by display ID with legacy ID fallback, store one effective draft per conversation, return raw `{ has_draft, message }` JSON for show, and return empty `200 OK` for update/delete. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. +- Worktree status at this implementation checkpoint: conversation reporting events from `reference/chatwoot/config/routes.rb:166`, enterprise `Api::V1::Accounts::ConversationsController#reporting_events`, and `enterprise/app/views/api/v1/models/_reporting_event.json.jbuilder` are implemented. `GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events` is now tracked and routed, resolves conversations by display ID with legacy ID fallback, scopes reporting events by account and conversation, orders by `created_at ASC`, and returns the raw Chatwoot reporting-event array with nullable relation keys preserved. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack. - `go test ./...` passes. -- Route dump succeeds with `TOTAL: 932` after adding singular conversation draft message routes. +- Route dump succeeds with `TOTAL: 933` after adding the enterprise conversation reporting events route. - Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`. -- Tracked frontend-critical route audit covers 385 Chatwoot routes: 372 exact, 0 method-compatible, 13 parameter-compatible, 0 missing. The 13 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits, dashboard app member `:id` names, plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. +- Tracked frontend-critical route audit covers 386 Chatwoot routes: 373 exact, 0 method-compatible, 13 parameter-compatible, 0 missing. The 13 parameter-compatible routes are Gin-internal parameter-name differences for nested AgentCapacityPolicy users/inbox limits, dashboard app member `:id` names, plus the public article `.md`/`.png` suffixes served through the same external article route dispatcher. - `/api/v1/widget` stubs are burned down and public inbox/contact/conversation/message core flows are backed by real handlers. - Handler test stability fixes are committed into the baseline before feature parity work continues. - `.codegraph/` is generated indexing output and is not part of tracked product code. @@ -140,7 +140,7 @@ This table is the shortest authoritative handoff view. If an older lower section | Priority | Workstream | Current state | Next checkpoint | Commit close rule | | --- | --- | --- | --- | --- | | 1 | P3.2a invitation/confirmation mail parity | Implemented for current non-SSO reference behavior: profile resend is no longer a TODO-only log, new invited agents and unconfirmed invited profile resends generate reset-password invitation links, normal unconfirmed profile resends generate confirmation links, `users.unconfirmed_email` is modeled for email-update routing, and fakeable/environment SMTP mailers keep default tests offline. | Keep in Review; reopen only if reused frontend smoke or fresh reference evidence exposes additional Devise confirmation states outside excluded SSO/SAML/LDAP/OIDC variants. | Focused profile and agent invitation tests, combined handler/service/repository/router/migrate/app tests, full `go test ./...`, and `git diff --check` passed. | -| 2 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 385-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association and custom-domain challenge routes from `routes.rb:657-660`, Twilio callback routes from `routes.rb:639-640`, enterprise Twilio voice routes from `routes.rb:643-646`, root Linear/Shopify/Notion OAuth callback routes from `routes.rb:630/634/654`, root Twitter/Google/Microsoft/Instagram/TikTok callback routes from `routes.rb:626/649-652`, assignment policy routes from `routes.rb:306-313`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, account integration app/hook routes from `routes.rb:345-348`, account Dyte routes from `routes.rb:357-358`, dashboard app routes from `routes.rb:130`, canned response routes from `routes.rb:114`, notification subscription routes from `routes.rb:440`, team/team-member routes from `routes.rb:296-300`, conversation participant routes from `routes.rb:150`, conversation direct upload route from `routes.rb:151`, conversation draft message routes from `routes.rb:152`, and conversation inbox assistant route from `routes.rb:165` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, account Dyte create/join payload behavior, dashboard app raw payload/serializer behavior, canned response raw payload/search/delete behavior, notification subscription payload behavior, team update/frontend route behavior, conversation participant route/payload/final-set update behavior, conversation direct upload ActiveStorage behavior, conversation draft message Redis-key-equivalent behavior, conversation inbox assistant Copilot payload behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, assignment policy CRUD and inbox binding payloads, Twilio inbound/status callbacks, enterprise Twilio voice callbacks, Linear/Shopify/Notion root integration callbacks, Shopify OAuth auth redirects, root channel OAuth callbacks, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, Cloudflare custom hostname verification, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Continue the next evidence-backed route/controller/serializer drift after conversation draft message parity or from B12 findings. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | +| 2 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 386-route critical set; dashboard `/app` shell routes from `routes.rb:19-20`, `.well-known` app association and custom-domain challenge routes from `routes.rb:657-660`, Twilio callback routes from `routes.rb:639-640`, enterprise Twilio voice routes from `routes.rb:643-646`, root Linear/Shopify/Notion OAuth callback routes from `routes.rb:630/634/654`, root Twitter/Google/Microsoft/Instagram/TikTok callback routes from `routes.rb:626/649-652`, assignment policy routes from `routes.rb:306-313`, help-center portal/category/article routes from `routes.rb:385-404`, public help-center portal/sitemap/article/category/search/article-detail routes from `routes.rb:590-601`, enterprise contact outbound voice call from `routes.rb:216`, account agent-bot routes from `routes.rb:94-97`, account webhook routes from `routes.rb:342`, account integration app/hook routes from `routes.rb:345-348`, account Dyte routes from `routes.rb:357-358`, dashboard app routes from `routes.rb:130`, canned response routes from `routes.rb:114`, notification subscription routes from `routes.rb:440`, team/team-member routes from `routes.rb:296-300`, conversation participant routes from `routes.rb:150`, conversation direct upload route from `routes.rb:151`, conversation draft message routes from `routes.rb:152`, conversation inbox assistant route from `routes.rb:165`, and conversation reporting events route from `routes.rb:166` are now explicitly tracked. Notification list/action serializers, user notification-settings raw payloads, campaigns raw payload/display-id routes, Devise password reset/confirmation payloads, CRM shared attachment payloads plus fixed 100-row attachment pagination, account/settings payloads, assignable-agent payloads, agent index full-list behavior, agent create/update/delete defaults/errors/scope, account agent-bot route/payload/mutation behavior, account webhook payload/mutation behavior, integration app/hook payload behavior, account Dyte create/join payload behavior, dashboard app raw payload/serializer behavior, canned response raw payload/search/delete behavior, notification subscription payload behavior, team update/frontend route behavior, conversation participant route/payload/final-set update behavior, conversation direct upload ActiveStorage behavior, conversation draft message Redis-key-equivalent behavior, conversation inbox assistant Copilot payload behavior, conversation reporting-event raw array behavior, label CRUD payloads, custom filters, custom attribute definitions, contact outbound voice calls, assignment policy CRUD and inbox binding payloads, Twilio inbound/status callbacks, enterprise Twilio voice callbacks, Linear/Shopify/Notion root integration callbacks, Shopify OAuth auth redirects, root channel OAuth callbacks, help-center portal/category/article payloads, dashboard app shell route behavior, app association JSON payloads, Cloudflare custom hostname verification, public widget popular-article lists, public help-center category list/show payloads, public portal show/default-locale payloads, public portal search payloads, public article show/markdown/tracking routes, and public sitemap XML now match the inspected Chatwoot contract. | Continue the next evidence-backed route/controller/serializer drift after conversation reporting-events parity or from B12 findings. | Regenerate parity artifacts when routes change and add endpoint-family fixture tests. | | 3 | Phase 6 placeholder audit | Widget/public/webhook critical placeholders are burned down; inbox WhatsApp health/register-webhook and sync-template drift are closed; refreshed `docs/parity/placeholder_audit.md` shows only webhook nil-handler fallbacks still call `chatwootParityStub`; dashboard conversation transcript/custom-attribute response drift and message retry status drift are closed. | Keep in Review; reopen only if fresh `rg`, route smoke, or B12 finds a frontend-reachable placeholder/stub in account/contact/conversation/message/inbox/widget/public paths. | `rg` placeholder audit and `scripts/parity_frontend_smoke.sh --check` are recorded; no reused-frontend blocker is ownerless. | | 4 | P3.9 account agent-bot API | Implemented for the reused dashboard AgentBots settings route with no-trailing-slash routes, PATCH update, raw Jbuilder-style payloads, account mutation scope, system-bot show/list visibility, empty `200 OK` delete, and full reset/avatar action payloads. | Keep in Review; reopen only if live settings smoke exposes avatar upload storage or administrator-secret gating drift. | Focused AgentBot handler tests, service/router focused tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 5 | P3.10 account webhooks API | Implemented for the reused dashboard Webhooks settings route with PATCH update, Chatwoot `{ payload }` list/mutation serializers, nested `{ webhook: ... }` bodies, generated secret, account-scoped mutations, URL/subscription validation, optional inbox serialization, and empty `200 OK` delete. | Keep in Review; reopen only if live settings smoke exposes audit writer or delivery-signature drift beyond the existing delivery service boundary. | Focused webhook handler/service/router tests, migration test, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | @@ -154,6 +154,7 @@ This table is the shortest authoritative handoff view. If an older lower section | 13 | P3.18 conversation direct uploads API | Implemented for reused dashboard composer uploads: `POST /conversations/:conversation_id/direct_uploads` is tracked and registered, ActiveStorage blob metadata returns raw `signed_id/direct_upload` JSON, the returned local PUT URL stores bytes under account-scoped direct uploads, and conversation lookup uses display ID with legacy ID fallback. | Keep in Review; reopen only if live composer upload smoke exposes ActiveStorage header/status/storage drift beyond the inspected controller/frontend contract. | Focused upload handler/service/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 14 | P3.19 conversation inbox assistant API | Implemented for reused dashboard Copilot state: `GET /conversations/:conversation_id/inbox_assistant` is tracked and registered, conversation lookup uses display ID with legacy ID fallback, account-scoped `captain_inboxes` binds the inbox to a Captain assistant, and responses are exactly `{ assistant: { id, name } }` or `{ assistant: null }`. | Keep in Review; reopen only if live conversation/Copilot smoke exposes feature-flag, authorization, or serializer drift beyond the inspected enterprise controller/store contract. | Focused ConversationCrud handler tests, service/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 15 | P3.20 conversation draft messages API | Implemented for Chatwoot's singular draft-message route: `GET/PATCH/PUT/DELETE /conversations/:conversation_id/draft_messages` are tracked and registered without a draft ID, conversation lookup uses display ID with legacy ID fallback, show returns raw `{ has_draft, message }`, update upserts the nested `draft_message.message`, and delete returns empty `200 OK`. | Keep in Review; reopen only if live conversation composer smoke exposes Redis-backed multi-agent or authorization drift beyond the inspected controller/spec contract. | Focused DraftMessage handler/service/repository/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | +| 16 | P3.21 conversation reporting events API | Implemented for Chatwoot enterprise conversation reporting events: `GET /conversations/:conversation_id/reporting_events` is tracked and registered, conversation lookup uses display ID with legacy ID fallback, events are account/conversation scoped and ordered by `created_at ASC`, and the response is the raw `_reporting_event` array with `id/name/value/value_in_business_hours/event_start_time/event_end_time/account_id/inbox_id/user_id/conversation_id/created_at/updated_at`. | Keep in Review; reopen only if live enterprise conversation analytics smoke exposes pagination, authorization, or serializer drift beyond the inspected enterprise controller/Jbuilder contract. | Focused Conversation handler tests, combined handler/service/repository/router/route-parity tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 16 | P6.8 contact outbound voice call | Implemented for the reused dashboard route with Twilio voice-enabled inbox validation, assigned-inbox check, open-conversation reuse, ContactInbox/conversation/call/message persistence, and Chatwoot response shape. | Keep in Review; reopen only if live smoke exposes provider initiation/status-update drift beyond the fakeable persisted boundary. | Focused contact call tests, route dump/parity regeneration, full `go test ./...`, and `git diff --check` passed. | | 17 | 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. | | 18 | 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. | @@ -189,6 +190,7 @@ These rows are the executable development plan from this point forward. A checkp | P3.18 conversation direct uploads API parity | `internal/router/router.go`, `internal/handler/api/v1/upload_handler.go`, `internal/service/upload_service.go`, `internal/app/bootstrap.go`, upload handler tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:151`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb`, `reference/chatwoot/app/javascript/dashboard/composables/useFileUpload.js`, `reference/chatwoot/app/javascript/dashboard/mixins/fileUploadMixin.js` | Conversation direct uploads now match the reused dashboard composer boundary: Chatwoot's nested `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads` route is tracked and registered, JSON ActiveStorage blob metadata returns raw `signed_id`, `direct_upload.url`, and headers, the returned local PUT target writes bytes to account-scoped storage, direct-upload rows keep `Source=account` and current account scope, and conversation resolution uses display ID with legacy ID fallback. | Review by `feat(conversations): align direct upload routes`; focused upload handler/service/router/route-parity tests, route dump/parity regeneration to `TOTAL: 927` and `367 exact, 13 parameter-compatible, 0 missing out of 380`, full `go test ./...`, and `git diff --check` passed. | | P3.19 conversation inbox assistant API parity | `internal/router/router.go`, `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, conversation CRUD/service tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:165`, `reference/chatwoot/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb`, `reference/chatwoot/app/javascript/dashboard/api/inbox/conversation.js`, `reference/chatwoot/app/javascript/dashboard/store/modules/conversations/actions.js`, conversation store mutation that writes `copilotAssistant` | Conversation inbox assistant now matches the reused dashboard Copilot sidebar contract: `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant` is tracked and registered, resolves conversations by account display ID with legacy ID fallback, joins `captain_inboxes` to `captain_assistants` with account and inbox scope, serializes only assistant `id/name`, and returns `{ assistant: null }` when no assistant is bound. | Review by `feat(conversations): expose inbox assistant`; focused ConversationCrud handler tests cover bound, unbound, and display-ID lookup paths; service/router/route-parity tests pass; route dump/parity regenerated to `TOTAL: 928` and `368 exact, 13 parameter-compatible, 0 missing out of 381`; full `go test ./...` and `git diff --check` passed. | | P3.20 conversation draft messages API parity | `internal/router/router.go`, `internal/handler/api/v1/draft_message_handler.go`, `internal/service/draft_message_service.go`, `internal/repository/draft_message_repo.go`, draft message handler tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:152`, `reference/chatwoot/app/controllers/api/v1/accounts/conversations/draft_messages_controller.rb`, `reference/chatwoot/spec/controllers/api/v1/accounts/conversations/draft_messages_controller_spec.rb`, `reference/chatwoot/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue` local draft call sites for composer behavior context | Conversation draft messages now match Chatwoot's singular route contract: `GET/PATCH/PUT/DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages` are tracked and registered without `:draft_id`, show returns raw `{ has_draft: false }` or `{ has_draft: true, message }`, update accepts nested `{ draft_message: { message } }` and returns empty `200 OK`, delete clears the conversation draft and returns empty `200 OK`, and conversation resolution uses display ID with legacy ID fallback. Legacy local collection/id draft routes remain only compatibility aliases. | Review by `feat(conversations): align draft message route`; focused DraftMessage handler tests cover raw show/no-draft, nested update, empty delete, display-ID lookup, and legacy route compatibility; service/repository/router/route-parity tests pass; route dump/parity regenerated to `TOTAL: 932` and `372 exact, 13 parameter-compatible, 0 missing out of 385`; full `go test ./...` and `git diff --check` passed. | +| P3.21 conversation reporting events API parity | `internal/router/router.go`, `internal/handler/api/v1/conversation_handler.go`, `internal/service/conversation_service.go`, `internal/repository/reporting_event_repo.go`, conversation handler tests, `cmd/route_parity` | `reference/chatwoot/config/routes.rb:166`, `reference/chatwoot/enterprise/app/controllers/enterprise/api/v1/accounts/conversations_controller.rb`, `reference/chatwoot/enterprise/app/views/api/v1/accounts/conversations/reporting_events.json.jbuilder`, `reference/chatwoot/enterprise/app/views/api/v1/models/_reporting_event.json.jbuilder`, `reference/chatwoot/app/models/reporting_event.rb` | Conversation reporting events now match Chatwoot's enterprise member route boundary: `GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events` is tracked and registered, resolves conversations by account display ID with legacy ID fallback, scopes event rows by account and conversation, orders by `created_at ASC`, and serializes a raw array with nullable `inbox_id`, `user_id`, and `conversation_id` keys preserved. | Review by `feat(conversations): expose reporting events`; focused Conversation handler tests cover raw array shape, ascending order, display-ID lookup, invalid params, and cross-account non-leakage; route dump/parity regenerated to `TOTAL: 933` and `373 exact, 13 parameter-compatible, 0 missing out of 386`; full `go test ./...` and `git diff --check` passed. | | P6.8 contact outbound voice call parity | `internal/router/router.go`, `internal/handler/api/v1/contact_handler.go`, `internal/service/contact_service.go`, `internal/model/call.go`, contact handler tests | `reference/chatwoot/config/routes.rb:216`, `reference/chatwoot/enterprise/app/controllers/api/v1/accounts/contacts/calls_controller.rb`, `reference/chatwoot/enterprise/app/services/voice/outbound_call_builder.rb`, `reference/chatwoot/enterprise/app/services/voice/call_message_builder.rb`, `reference/chatwoot/enterprise/app/models/call.rb`, dashboard `api/contacts.js`, `store/modules/contacts/actions.js`, `api/channel/voice/voiceAPIClient.js` | Contact outbound calls now match the Chatwoot enterprise route boundary: account contact lookup, current user's assigned `Channel::TwilioSms` inbox lookup, `voice_enabled` guard, phone-number guard, open display-ID conversation reuse only for same inbox/contact, new ContactInbox/open conversation creation when needed, persisted outgoing Twilio call metadata, linked `voice_call` message content attributes, and raw `{ conversation_id, inbox_id, call_sid, conference_sid }` response. | Review by `feat(contacts): initiate voice calls`; focused contact call tests cover success/reuse/resolved-hint ignored/no-phone/non-voice/unassigned cases; route dump/parity regenerated to `TOTAL: 861` and `299 exact, 7 parameter-compatible, 0 missing out of 306`; full `go test ./...` and `git diff --check` passed. | | 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. Empty/null JSON serializes as `{}`. | Review by `feat(conversations): align custom attribute response`; focused handler tests, combined handler/service/router tests, full `go test ./...`, and `git diff --check` passed. | @@ -230,6 +232,7 @@ This ledger records the committed parity checkpoints that future slices should b | Commit | Scope | Verification summary | Follow-up state | | --- | --- | --- | --- | +| `feat(conversations): expose reporting events` | Advances P3.21 conversation reporting-events parity by matching Chatwoot enterprise `Api::V1::Accounts::ConversationsController#reporting_events`, route `166`, and the `_reporting_event` Jbuilder partial. GoChat now registers and tracks `GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events`, resolves conversations by display ID with legacy ID fallback, scopes reporting events by account and conversation, orders by `created_at ASC`, and returns the raw Chatwoot array shape with nullable relation keys preserved. | `go test ./internal/handler/api/v1 ./internal/service ./internal/repository ./internal/router ./cmd/route_parity -run 'ReportingEvent\|Conversation\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 933`; tracked route parity is `373 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 386`. | P3.21 moves to Review for current enterprise conversation reporting-event evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | | `feat(conversations): align draft message route` | Advances P3.20 conversation draft-message parity by matching Chatwoot `Api::V1::Accounts::Conversations::DraftMessagesController`, route `152`, and its request specs. GoChat now registers and tracks the singular `GET/PATCH/PUT/DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages` routes without a draft ID, resolves conversations by display ID with legacy ID fallback, models the Redis key behavior as one effective persisted draft per conversation, returns raw `{ has_draft, message }` show payloads, accepts nested `{ draft_message: { message } }` updates, and returns empty `200 OK` for update/delete. | `go test ./internal/handler/api/v1 ./internal/service ./internal/repository ./internal/router ./cmd/route_parity -run 'DraftMessage\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 932`; tracked route parity is `372 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 385`. | P3.20 moves to Review for current conversation draft-message evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | | `feat(conversations): expose inbox assistant` | Advances P3.19 conversation inbox assistant parity by matching Chatwoot enterprise `Api::V1::Accounts::ConversationsController#inbox_assistant`, route `165`, and reused dashboard `api/inbox/conversation.js` plus conversation store Copilot assistant state. GoChat now registers and tracks `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant`, resolves conversations by display ID with legacy ID fallback, account-scopes the inbox-to-Captain-assistant join through `captain_inboxes` and `captain_assistants`, returns only assistant `id/name`, and returns `{ assistant: null }` for unbound inboxes. | `go test ./internal/handler/api/v1 ./internal/service ./internal/router ./cmd/route_parity -run 'ConversationCrud\|Conversation\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 928`; tracked route parity is `368 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 381`. | P3.19 moves to Review for current conversation Copilot assistant evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | | `feat(conversations): align direct upload routes` | Advances P3.18 conversation direct upload parity by matching Chatwoot nested `Conversations::DirectUploadsController`, route `151`, and the reused dashboard ActiveStorage upload callers. GoChat now registers and tracks `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads`, returns raw ActiveStorage `signed_id/direct_upload` metadata, validates the account-scoped conversation by display ID with legacy ID fallback, stores upload rows as account direct uploads, and exposes the local PUT URL used by the metadata response to persist bytes. | `go test ./internal/handler/api/v1 ./internal/service ./internal/router ./cmd/route_parity -run 'Upload\|Router\|RouteParity' -count=1`; `go run ./cmd/dump_routes > docs/parity/gochat_routes.txt`; `go run ./cmd/route_parity`; full `go test ./...`; `git diff --check`. Route dump is `TOTAL: 927`; tracked route parity is `367 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 380`. | P3.18 moves to Review for current composer upload evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. | @@ -2421,3 +2424,4 @@ Verification milestone gates: - 2026-06-06: P3.18 conversation direct upload checkpoint prepared as `feat(conversations): align direct upload routes`; audited Chatwoot nested direct upload route/controller and reused dashboard ActiveStorage upload callers. GoChat now exposes and tracks `POST /api/v1/accounts/:account_id/conversations/:conversation_id/direct_uploads`, returns raw ActiveStorage `signed_id/direct_upload` metadata, validates account-scoped conversations by display ID with legacy ID fallback, and supports the returned local PUT upload target for account-scoped bytes. Focused upload handler/service/router/route-parity tests, route dump/parity regeneration (`TOTAL: 927`, `367 exact`, `13 parameter-compatible`, `0 missing out of 380`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. - 2026-06-06: P3.19 conversation inbox assistant checkpoint prepared as `feat(conversations): expose inbox assistant`; audited Chatwoot enterprise conversation `inbox_assistant`, route `165`, and reused dashboard conversation API/store Copilot assistant state. GoChat now exposes and tracks `GET /api/v1/accounts/:account_id/conversations/:conversation_id/inbox_assistant`, resolves conversations by display ID with legacy ID fallback, looks up the account-scoped Captain assistant bound to the conversation inbox, and returns `{ assistant: { id, name } }` or `{ assistant: null }`. Focused ConversationCrud handler/service/router/route-parity tests, route dump/parity regeneration (`TOTAL: 928`, `368 exact`, `13 parameter-compatible`, `0 missing out of 381`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. - 2026-06-06: P3.20 conversation draft message checkpoint prepared as `feat(conversations): align draft message route`; audited Chatwoot singular draft message route/controller/specs. GoChat now exposes and tracks `GET/PATCH/PUT/DELETE /api/v1/accounts/:account_id/conversations/:conversation_id/draft_messages` without a draft ID, resolves conversations by display ID with legacy ID fallback, persists one effective draft per conversation, returns raw `{ has_draft, message }` show payloads, accepts nested `draft_message.message`, and returns empty `200 OK` for update/delete. Focused DraftMessage handler/service/repository/router/route-parity tests, route dump/parity regeneration (`TOTAL: 932`, `372 exact`, `13 parameter-compatible`, `0 missing out of 385`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. +- 2026-06-06: P3.21 conversation reporting events checkpoint prepared as `feat(conversations): expose reporting events`; audited Chatwoot enterprise conversation `reporting_events`, route `166`, and `_reporting_event` Jbuilder serializer. GoChat now exposes and tracks `GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events`, resolves conversations by display ID with legacy ID fallback, scopes raw reporting-event rows by account and conversation, orders by `created_at ASC`, and returns the raw Chatwoot event array with nullable relation keys preserved. Focused Conversation handler tests, route dump/parity regeneration (`TOTAL: 933`, `373 exact`, `13 parameter-compatible`, `0 missing out of 386`), full `go test ./...`, and `git diff --check` passed; continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke. diff --git a/docs/parity/gochat_routes.txt b/docs/parity/gochat_routes.txt index 8d127224..50918ae6 100644 --- a/docs/parity/gochat_routes.txt +++ b/docs/parity/gochat_routes.txt @@ -227,6 +227,7 @@ GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:messag GET /api/v1/accounts/:account_id/conversations/:conversation_id/messages/:message_id/delivery_status/ GET /api/v1/accounts/:account_id/conversations/:conversation_id/participants GET /api/v1/accounts/:account_id/conversations/:conversation_id/participants/ +GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events GET /api/v1/accounts/:account_id/conversations/:conversation_id/whatsapp_calls/ GET /api/v1/accounts/:account_id/conversations/:conversation_id/whatsapp_calls/:call_id GET /api/v1/accounts/:account_id/conversations/meta @@ -930,4 +931,4 @@ PUT /public/api/v1/csat_survey/:id PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id PUT /public/api/v1/inboxes/:inbox_id/contacts/:contact_id/conversations/:conversation_id/messages/:message_id PUT /widget/direct_uploads/:upload_uuid -TOTAL: 932 +TOTAL: 933 diff --git a/docs/parity/route_parity.md b/docs/parity/route_parity.md index 69404ef4..38ce5721 100644 --- a/docs/parity/route_parity.md +++ b/docs/parity/route_parity.md @@ -7,7 +7,7 @@ Generated from: This report covers tracked frontend-critical Chatwoot routes from `reference/chatwoot/config/routes.rb`, including API v1 account routes, Captain/Copilot, assignment policies, widget/public APIs, and API v2 reports. Ruby is not installed in the workspace, so Chatwoot routes are sourced from static route declarations instead of `bin/rails routes`. -Summary: 372 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 385 tracked critical routes. +Summary: 373 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 386 tracked critical routes. ## Missing Critical Routes @@ -150,6 +150,7 @@ These routes exist with equivalent method and path shape but different parameter | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/labels` | `/api/v1/accounts/:account_id/conversations/:conversation_id/labels` | `api/v1/accounts/conversations/labels#index` | `routes.rb:149` | exact | | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/` | `/api/v1/accounts/:account_id/conversations/:conversation_id/messages/` | `api/v1/accounts/conversations/messages#index` | `routes.rb:142` | exact | | GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `/api/v1/accounts/:account_id/conversations/:conversation_id/participants` | `api/v1/accounts/conversations/participants#show` | `routes.rb:150` | exact | +| GET | `/api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events` | `/api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events` | `api/v1/accounts/conversations#reporting_events` | `routes.rb:166` | exact | | GET | `/api/v1/accounts/:account_id/conversations/meta` | `/api/v1/accounts/:account_id/conversations/meta` | `api/v1/accounts/conversations#meta` | `routes.rb:136` | exact | | GET | `/api/v1/accounts/:account_id/conversations/search` | `/api/v1/accounts/:account_id/conversations/search` | `api/v1/accounts/conversations#search` | `routes.rb:137` | exact | | GET | `/api/v1/accounts/:account_id/conversations/unread_counts` | `/api/v1/accounts/:account_id/conversations/unread_counts` | `api/v1/accounts/conversations/unread_counts#index` | `routes.rb:138` | exact | diff --git a/internal/handler/api/v1/conversation_handler.go b/internal/handler/api/v1/conversation_handler.go index e46e75ff..94bb3a2b 100644 --- a/internal/handler/api/v1/conversation_handler.go +++ b/internal/handler/api/v1/conversation_handler.go @@ -811,6 +811,52 @@ func (h *ConversationHandler) InboxAssistant(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"assistant": gin.H{"id": assistant.ID, "name": assistant.Name}}) } +// ReportingEvents returns raw reporting events for a conversation. +// GET /api/v1/accounts/:account_id/conversations/:conversation_id/reporting_events +// Reference: Chatwoot enterprise conversations_controller#reporting_events. +func (h *ConversationHandler) ReportingEvents(c *gin.Context) { + accountID, err := parseUintParam(c, "account_id") + if err != nil { + response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") + return + } + + conversationID, err := parseUintParam(c, "conversation_id") + if err != nil { + response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation_id") + return + } + + events, svcErr := h.conversationSvc.ListReportingEvents(c.Request.Context(), accountID, conversationID) + if svcErr != nil { + handleServiceError(c, svcErr) + return + } + + payload := make([]gin.H, 0, len(events)) + for i := range events { + payload = append(payload, serializeReportingEvent(events[i])) + } + c.JSON(http.StatusOK, payload) +} + +func serializeReportingEvent(event model.ReportingEvent) gin.H { + return gin.H{ + "id": event.ID, + "name": event.Name, + "value": event.Value, + "value_in_business_hours": event.ValueInBusinessHours, + "event_start_time": event.EventStartTime, + "event_end_time": event.EventEndTime, + "account_id": event.AccountID, + "inbox_id": event.InboxID, + "user_id": event.UserID, + "conversation_id": event.ConversationID, + "created_at": event.CreatedAt, + "updated_at": event.UpdatedAt, + } +} + // ToggleTyping toggles the typing status for an agent in a conversation. // POST /api/v1/accounts/:account_id/conversations/:id/toggle_typing_status // Reference: Chatwoot conversations_controller#toggle_typing_status (member route) diff --git a/internal/handler/api/v1/conversation_handler_test.go b/internal/handler/api/v1/conversation_handler_test.go index 9ba9797e..e19652a9 100644 --- a/internal/handler/api/v1/conversation_handler_test.go +++ b/internal/handler/api/v1/conversation_handler_test.go @@ -68,6 +68,7 @@ func (s *ConversationHandlerTestSuite) SetupSuite() { &model.ContactInbox{}, &model.Conversation{}, &model.ConversationParticipant{}, + &model.ReportingEvent{}, &model.Message{}, &model.Attachment{}, &model.InboxMember{}, @@ -122,6 +123,7 @@ func (s *ConversationHandlerTestSuite) SetupSuite() { conversations.POST("/:conversation_id/transcript", handler.Transcript) conversations.POST("/:conversation_id/custom_attributes", handler.UpdateCustomAttributes) conversations.GET("/:conversation_id/attachments", handler.ListAttachments) + conversations.GET("/:conversation_id/reporting_events", handler.ReportingEvents) conversations.POST("/:conversation_id/toggle_typing", handler.ToggleTyping) conversations.POST("/:conversation_id/update_last_seen", handler.UpdateLastSeen) } @@ -137,6 +139,7 @@ func (s *ConversationHandlerTestSuite) TearDownSuite() { func (s *ConversationHandlerTestSuite) TearDownTest() { s.db.Exec("DELETE FROM attachments") + s.db.Exec("DELETE FROM reporting_events") s.db.Exec("DELETE FROM conversations") s.db.Exec("DELETE FROM contact_inboxes") s.db.Exec("DELETE FROM contacts") @@ -504,6 +507,102 @@ func (s *ConversationHandlerTestSuite) TestListAttachmentsUsesChatwootFixedPageS assert.Equal(s.T(), float64(newestID), first["id"]) } +func (s *ConversationHandlerTestSuite) TestReportingEventsReturnsRawArrayInCreatedOrder() { + conversationID := s.testConv.ID + baseTime := time.Now().Add(-2 * time.Hour).UTC() + newer := &model.ReportingEvent{ + Base: model.Base{CreatedAt: baseTime.Add(time.Hour), UpdatedAt: baseTime.Add(time.Hour)}, + AccountID: s.testAccount.ID, + Name: "resolution_time", + Value: 42, + ValueInBusinessHours: 21, + ConversationID: &conversationID, + EventStartTime: baseTime, + EventEndTime: baseTime.Add(time.Minute), + } + older := &model.ReportingEvent{ + Base: model.Base{CreatedAt: baseTime, UpdatedAt: baseTime}, + AccountID: s.testAccount.ID, + Name: "first_response", + Value: 10, + ValueInBusinessHours: 5, + ConversationID: &conversationID, + EventStartTime: baseTime.Add(-time.Minute), + EventEndTime: baseTime, + } + s.Require().NoError(s.db.Create(newer).Error) + s.Require().NoError(s.db.Create(older).Error) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", fmt.Sprintf("%s/conversations/%d/reporting_events", s.accountURL(), s.testConv.ID), nil) + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String()) + var resp []map[string]any + assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Len(s.T(), resp, 2) + assert.Equal(s.T(), float64(older.ID), resp[0]["id"]) + assert.Equal(s.T(), "first_response", resp[0]["name"]) + assert.Equal(s.T(), float64(10), resp[0]["value"]) + assert.Equal(s.T(), float64(5), resp[0]["value_in_business_hours"]) + assert.Equal(s.T(), float64(s.testAccount.ID), resp[0]["account_id"]) + assert.Equal(s.T(), float64(s.testConv.ID), resp[0]["conversation_id"]) + assert.Contains(s.T(), resp[0], "inbox_id") + assert.Nil(s.T(), resp[0]["inbox_id"]) + assert.Contains(s.T(), resp[0], "user_id") + assert.Nil(s.T(), resp[0]["user_id"]) + assert.Contains(s.T(), resp[0], "event_start_time") + assert.Contains(s.T(), resp[0], "event_end_time") + assert.NotContains(s.T(), resp[0], "success") + assert.NotContains(s.T(), resp[0], "payload") + assert.NotContains(s.T(), resp[0], "meta") +} + +func (s *ConversationHandlerTestSuite) TestReportingEventsUsesDisplayIDAndScopesAccount() { + displayID := uint(880) + s.Require().NoError(s.db.Model(s.testConv).Update("display_id", displayID).Error) + conversationID := s.testConv.ID + event := &model.ReportingEvent{ + AccountID: s.testAccount.ID, + Name: "reply_time", + Value: 7, + ValueInBusinessHours: 3, + ConversationID: &conversationID, + EventStartTime: time.Now().Add(-time.Minute).UTC(), + EventEndTime: time.Now().UTC(), + } + s.Require().NoError(s.db.Create(event).Error) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", fmt.Sprintf("%s/conversations/%d/reporting_events", s.accountURL(), displayID), nil) + s.router.ServeHTTP(w, req) + + assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String()) + var resp []map[string]any + assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) + assert.Len(s.T(), resp, 1) + assert.Equal(s.T(), float64(event.ID), resp[0]["id"]) + + otherAccount := &model.Account{Name: "OtherOrg", Locale: "en", Active: true} + s.Require().NoError(s.db.Create(otherAccount).Error) + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/conversations/%d/reporting_events", otherAccount.ID, displayID), nil) + s.router.ServeHTTP(w, req) + assert.Equal(s.T(), http.StatusNotFound, w.Code) +} + +func (s *ConversationHandlerTestSuite) TestReportingEventsInvalidParams() { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/conversations/1/reporting_events", nil) + s.router.ServeHTTP(w, req) + assert.Equal(s.T(), http.StatusBadRequest, w.Code) + + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/conversations/abc/reporting_events", s.testAccount.ID), nil) + s.router.ServeHTTP(w, req) + assert.Equal(s.T(), http.StatusBadRequest, w.Code) +} + func (s *ConversationHandlerTestSuite) TestToggleTyping_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations/1/toggle_typing", bytes.NewBufferString(`{"typing_status":"on"}`)) diff --git a/internal/repository/reporting_event_repo.go b/internal/repository/reporting_event_repo.go index c7214757..001d469c 100644 --- a/internal/repository/reporting_event_repo.go +++ b/internal/repository/reporting_event_repo.go @@ -60,6 +60,15 @@ func (r *ReportingEventRepo) FindByInboxID(ctx context.Context, accountID, inbox return events, err } +func (r *ReportingEventRepo) FindByConversationID(ctx context.Context, accountID, conversationID uint) ([]model.ReportingEvent, error) { + var events []model.ReportingEvent + err := r.db.WithContext(ctx). + Where("account_id = ? AND conversation_id = ?", accountID, conversationID). + Order("created_at ASC"). + Find(&events).Error + return events, err +} + // AggregateByMetric returns aggregated values (average, count) for a given metric name. func (r *ReportingEventRepo) AggregateByMetric(ctx context.Context, accountID uint, metricName string, since, until time.Time) (float64, int64, error) { var result struct { diff --git a/internal/router/router.go b/internal/router/router.go index 1877cf31..589123fc 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -977,6 +977,7 @@ func registerV1Routes(g *gin.RouterGroup, h *Handlers) { // Additional Conversation endpoints matching Chatwoot member routes conversations.GET("/:conversation_id/attachments", h.Conversation.ListAttachments) conversations.GET("/:conversation_id/inbox_assistant", h.Conversation.InboxAssistant) + conversations.GET("/:conversation_id/reporting_events", h.Conversation.ReportingEvents) conversations.POST("/:conversation_id/direct_uploads", h.Upload.ConversationDirectUpload) conversations.PUT("/:conversation_id/direct_uploads/:upload_uuid", h.Upload.CompleteConversationDirectUpload) conversations.POST("/:conversation_id/toggle_typing_status", h.Conversation.ToggleTyping) diff --git a/internal/service/conversation_service.go b/internal/service/conversation_service.go index 875dc729..eb7a3466 100644 --- a/internal/service/conversation_service.go +++ b/internal/service/conversation_service.go @@ -184,6 +184,22 @@ func (s *ConversationService) GetInboxAssistant(ctx context.Context, accountID, return &assistant, nil } +// ListReportingEvents returns conversation-scoped reporting events in Chatwoot order. +// Reference: Chatwoot enterprise ConversationsController#reporting_events. +func (s *ConversationService) ListReportingEvents(ctx context.Context, accountID, routeID uint) ([]model.ReportingEvent, error) { + conversation, err := s.repo.FindByAccountAndDisplayIDOrID(ctx, accountID, routeID) + if err != nil { + return nil, err + } + + var events []model.ReportingEvent + err = s.repo.DB().WithContext(ctx). + Where("account_id = ? AND conversation_id = ?", accountID, conversation.ID). + Order("created_at ASC"). + Find(&events).Error + return events, err +} + // CreateConversationRequest is the DTO for creating a conversation. // Reference: Chatwoot app/controllers/api/v1/accounts/conversations_controller.rb #create // Supports creating conversation with an initial message (like Chatwoot's ConversationBuilder).