feat(reporting): align account events

This commit is contained in:
2026-06-06 15:53:33 +08:00
parent 0631249224
commit 2072396f12
7 changed files with 292 additions and 44 deletions
+1
View File
@@ -300,6 +300,7 @@ var criticalRoutes = []route{
{Method: "GET", Path: "/api/v1/accounts/:account_id/applied_slas", Controller: "api/v1/accounts/applied_slas#index", Source: "routes.rb:228"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/applied_slas/metrics", Controller: "api/v1/accounts/applied_slas#metrics", Source: "routes.rb:230"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/applied_slas/download", Controller: "api/v1/accounts/applied_slas#download", Source: "routes.rb:231"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/reporting_events", Controller: "api/v1/accounts/reporting_events#index", Source: "routes.rb:234"},
{Method: "GET", Path: "/api/v1/accounts/:account_id/captain/preferences/", Controller: "api/v1/accounts/captain/preferences#show", Source: "routes.rb:63"},
{Method: "PUT", Path: "/api/v1/accounts/:account_id/captain/preferences/", Controller: "api/v1/accounts/captain/preferences#update", Source: "routes.rb:63"},
+11 -7
View File
@@ -49,15 +49,15 @@ Hermes task landing checklist:
## Current Baseline
- 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/*`.
- Current tracking checkpoint: 2026-06-06 account reporting events checkpoint, prepared as `feat(reporting): align account events`.
- Latest implementation checkpoint: this checkpoint, prepared as `feat(reporting): align account events`.
- Latest documentation/tooling checkpoint: this tracker update for P3.22 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 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.
- Worktree status at this implementation checkpoint: account reporting events from `reference/chatwoot/config/routes.rb:234`, enterprise `Api::V1::Accounts::ReportingEventsController#index`, and its index Jbuilder are implemented. `GET /api/v1/accounts/:account_id/reporting_events` now returns Chatwoot `{ payload, meta }`, filters by optional Unix `since/until`, `inbox_id`, `user_id`, and `name`, orders by `created_at DESC`, paginates at 25 rows per page, and reuses the `_reporting_event` raw serializer. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
- `go test ./...` passes.
- Route dump succeeds with `TOTAL: 933` after adding the enterprise conversation reporting events route.
- Route dump succeeds with `TOTAL: 933`; P3.22 changes account reporting-events behavior and expands tracked route parity without adding a new Go route.
- Route parity artifacts now exist under `docs/parity/` and are generated by `cmd/route_parity`.
- 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.
- Tracked frontend-critical route audit covers 387 Chatwoot routes: 374 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 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. |
| 2 | Phase 2/3 drift | Tracked route parity is 0 missing for the current 387-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`, conversation reporting events route from `routes.rb:166`, and account reporting events route from `routes.rb:234` 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, account reporting-events payload/filter/pagination 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 account 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. |
@@ -155,6 +155,7 @@ This table is the shortest authoritative handoff view. If an older lower section
| 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. |
| 17 | P3.22 account reporting events API | Implemented for Chatwoot enterprise account reporting events: `GET /reporting_events` is tracked and returns `{ payload, meta }`, accepts optional Unix `since/until`, `inbox_id`, `user_id`, and `name` filters, orders newest first, paginates at 25 rows per page, and serializes each row through the same `_reporting_event` field shape. | Keep in Review; reopen only if live enterprise reports/audit smoke exposes admin-authorization, includes, or date-range drift beyond the inspected controller/spec contract. | Focused ReportingEvent handler tests, combined handler/service/repository/router/route-parity tests, route 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. |
@@ -191,6 +192,7 @@ These rows are the executable development plan from this point forward. A checkp
| 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. |
| P3.22 account reporting events API parity | `cmd/route_parity`, `internal/handler/api/v1/reporting_event_handler.go`, `internal/service/reporting_event_service.go`, `internal/repository/reporting_event_repo.go`, reporting event handler tests | `reference/chatwoot/config/routes.rb:234`, `reference/chatwoot/enterprise/app/controllers/api/v1/accounts/reporting_events_controller.rb`, `reference/chatwoot/enterprise/app/views/api/v1/accounts/reporting_events/index.json.jbuilder`, `reference/chatwoot/enterprise/app/views/api/v1/models/_reporting_event.json.jbuilder`, `reference/chatwoot/spec/enterprise/controllers/api/v1/accounts/reporting_events_controller_spec.rb`, `reference/chatwoot/app/helpers/date_range_helper.rb` | Account reporting events now match Chatwoot's enterprise index boundary: `GET /api/v1/accounts/:account_id/reporting_events` is tracked, returns raw `{ payload, meta: { count, current_page, total_pages } }`, filters by optional Unix timestamp `since/until`, `inbox_id`, `user_id`, and `name`, orders by `created_at DESC`, uses a fixed 25-row page size, and serializes event rows with nullable relation keys preserved. | Review by `feat(reporting): align account events`; focused ReportingEvent handler tests cover payload shape, ordering, combined filters, pagination, and invalid filters; route parity regenerated to `374 exact, 13 parameter-compatible, 0 missing out of 387`; 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. |
@@ -232,6 +234,7 @@ This ledger records the committed parity checkpoints that future slices should b
| Commit | Scope | Verification summary | Follow-up state |
| --- | --- | --- | --- |
| `feat(reporting): align account events` | Advances P3.22 account reporting-events parity by matching Chatwoot enterprise `Api::V1::Accounts::ReportingEventsController#index`, route `234`, `DateRangeHelper`, request specs, and the `_reporting_event` Jbuilder partial. GoChat now returns `{ payload, meta }` instead of the local success envelope, treats date filtering as optional Unix `since/until`, supports `inbox_id`, `user_id`, and `name` filters, orders by `created_at DESC`, paginates at Chatwoot's fixed 25 rows per page, and tracks the account route in route parity. | `go test ./internal/handler/api/v1 ./internal/service ./internal/repository ./internal/router ./cmd/route_parity -run 'ReportingEvent\|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 `374 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 387`. | P3.22 moves to Review for current enterprise account reporting-events evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. |
| `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. |
@@ -2425,3 +2428,4 @@ Verification milestone gates:
- 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.
- 2026-06-06: P3.22 account reporting events checkpoint prepared as `feat(reporting): align account events`; audited Chatwoot enterprise account reporting events controller/specs, route `234`, `DateRangeHelper`, and `_reporting_event` Jbuilder serializer. GoChat now returns `{ payload, meta }`, accepts optional Unix `since/until`, `inbox_id`, `user_id`, and `name` filters, orders account reporting events by `created_at DESC`, uses fixed 25-row pagination, and tracks the account reporting-events route. Focused ReportingEvent handler tests and route parity regeneration (`TOTAL: 933`, `374 exact`, `13 parameter-compatible`, `0 missing out of 387`) passed; full `go test ./...` and `git diff --check` passed.
+2 -1
View File
@@ -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: 373 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 386 tracked critical routes.
Summary: 374 exact, 0 method-compatible, 13 parameter-compatible, 0 missing out of 387 tracked critical routes.
## Missing Critical Routes
@@ -187,6 +187,7 @@ These routes exist with equivalent method and path shape but different parameter
| GET | `/api/v1/accounts/:account_id/portals/:portal_id/categories` | `/api/v1/accounts/:account_id/portals/:portal_id/categories` | `api/v1/accounts/categories#index` | `routes.rb:392` | exact |
| GET | `/api/v1/accounts/:account_id/portals/:portal_id/categories/:category_id` | `/api/v1/accounts/:account_id/portals/:portal_id/categories/:category_id` | `api/v1/accounts/categories#show` | `routes.rb:392` | exact |
| GET | `/api/v1/accounts/:account_id/portals/:portal_id/ssl_status` | `/api/v1/accounts/:account_id/portals/:portal_id/ssl_status` | `api/v1/accounts/portals#ssl_status` | `routes.rb:390` | exact |
| GET | `/api/v1/accounts/:account_id/reporting_events` | `/api/v1/accounts/:account_id/reporting_events` | `api/v1/accounts/reporting_events#index` | `routes.rb:234` | exact |
| GET | `/api/v1/accounts/:account_id/search` | `/api/v1/accounts/:account_id/search` | `api/v1/accounts/search#index` | `routes.rb:170` | exact |
| GET | `/api/v1/accounts/:account_id/search/articles` | `/api/v1/accounts/:account_id/search/articles` | `api/v1/accounts/search#articles` | `routes.rb:176` | exact |
| GET | `/api/v1/accounts/:account_id/search/contacts` | `/api/v1/accounts/:account_id/search/contacts` | `api/v1/accounts/search#contacts` | `routes.rb:175` | exact |
@@ -2,6 +2,8 @@ package v1
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -20,8 +22,10 @@ func NewReportingEventHandler(svc *service.ReportingEventService) *ReportingEven
return &ReportingEventHandler{svc: svc}
}
// List retrieves reporting events for an account with optional date range filtering.
// GET /api/v1/accounts/:account_id/reporting_events?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z&metric=message_created
const chatwootReportingEventsPerPage = 25
// List retrieves reporting events for an account with Chatwoot filters and pagination.
// GET /api/v1/accounts/:account_id/reporting_events?since=...&until=...&inbox_id=...&user_id=...&name=...&page=...
func (h *ReportingEventHandler) List(c *gin.Context) {
accountID, err := parseUintParam(c, "account_id")
if err != nil {
@@ -29,29 +33,81 @@ func (h *ReportingEventHandler) List(c *gin.Context) {
return
}
since, until, ok := parseDateRange(c)
filter, ok := parseReportingEventListFilter(c)
if !ok {
return
}
// Check for optional metric filter
metric := c.Query("metric")
if metric != "" {
events, svcErr := h.svc.GetByMetric(c.Request.Context(), accountID, metric, since, until)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, gin.H{"reporting_events": events})
return
}
events, svcErr := h.svc.ListByAccount(c.Request.Context(), accountID, since, until)
result, svcErr := h.svc.ListAccountEvents(c.Request.Context(), accountID, filter)
if svcErr != nil {
handleServiceError(c, svcErr)
return
}
response.OK(c, gin.H{"reporting_events": events})
}
payload := make([]gin.H, 0, len(result.Events))
for i := range result.Events {
payload = append(payload, serializeReportingEvent(result.Events[i]))
}
c.JSON(http.StatusOK, gin.H{
"payload": payload,
"meta": gin.H{
"count": result.Total,
"current_page": result.CurrentPage,
"total_pages": result.TotalPages,
},
})
}
func parseReportingEventListFilter(c *gin.Context) (service.ReportingEventListFilter, bool) {
page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
if err != nil || page < 1 {
page = 1
}
inboxID, err := parseOptionalUintQueryParam(c, "inbox_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid inbox_id")
return service.ReportingEventListFilter{}, false
}
userID, err := parseOptionalUintQueryParam(c, "user_id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user_id")
return service.ReportingEventListFilter{}, false
}
filter := service.ReportingEventListFilter{
InboxID: inboxID,
UserID: userID,
Name: c.Query("name"),
Page: page,
PerPage: chatwootReportingEventsPerPage,
}
sinceStr := c.Query("since")
untilStr := c.Query("until")
if sinceStr == "" || untilStr == "" {
return filter, true
}
since, err := parseUnixTimestamp(sinceStr)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format")
return service.ReportingEventListFilter{}, false
}
until, err := parseUnixTimestamp(untilStr)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format")
return service.ReportingEventListFilter{}, false
}
filter.Since = &since
filter.Until = &until
return filter, true
}
func parseUnixTimestamp(value string) (time.Time, error) {
seconds, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return time.Time{}, err
}
return time.Unix(seconds, 0).UTC(), nil
}
@@ -1,9 +1,12 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
@@ -41,6 +44,10 @@ func TestReportingEventHandlerTestSuite(t *testing.T) {
suite.Run(t, new(ReportingEventHandlerTestSuite))
}
func (s *ReportingEventHandlerTestSuite) TearDownTest() {
s.db.Exec("DELETE FROM reporting_events")
}
func (s *ReportingEventHandlerTestSuite) TestList_InvalidAccountID() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/reporting_events?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", nil)
@@ -48,26 +55,124 @@ func (s *ReportingEventHandlerTestSuite) TestList_InvalidAccountID() {
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *ReportingEventHandlerTestSuite) TestList_MissingDateRange() {
func (s *ReportingEventHandlerTestSuite) TestList_ReturnsChatwootPayloadSortedByCreatedAtDesc() {
baseTime := time.Now().Add(-3 * time.Hour).UTC()
oldEvent := s.createEvent(1, "first_response", baseTime, nil, nil)
_ = s.createEvent(1, "resolution", baseTime.Add(time.Hour), nil, nil)
newEvent := s.createEvent(1, "reply_time", baseTime.Add(2*time.Hour), nil, nil)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code, w.Body.String())
var resp map[string]any
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Contains(resp, "payload")
s.Contains(resp, "meta")
s.NotContains(resp, "success")
meta := resp["meta"].(map[string]any)
s.Equal(float64(3), meta["count"])
s.Equal(float64(1), meta["current_page"])
s.Equal(float64(1), meta["total_pages"])
payload := resp["payload"].([]any)
s.Len(payload, 3)
first := payload[0].(map[string]any)
last := payload[2].(map[string]any)
s.Equal(float64(newEvent.ID), first["id"])
s.Equal("reply_time", first["name"])
s.Equal(float64(oldEvent.ID), last["id"])
s.Equal("first_response", last["name"])
s.Contains(first, "value_in_business_hours")
s.Contains(first, "event_start_time")
s.Contains(first, "event_end_time")
}
func (s *ReportingEventHandlerTestSuite) TestList_FiltersByUnixDateRangeInboxUserAndName() {
baseTime := time.Now().Add(-4 * time.Hour).UTC().Truncate(time.Second)
inboxID := uint(10)
otherInboxID := uint(11)
userID := uint(20)
otherUserID := uint(21)
matched := s.createEvent(1, "first_response", baseTime.Add(2*time.Hour), &inboxID, &userID)
s.createEvent(1, "first_response", baseTime.Add(2*time.Hour), &otherInboxID, &userID)
s.createEvent(1, "first_response", baseTime.Add(2*time.Hour), &inboxID, &otherUserID)
s.createEvent(1, "reply_time", baseTime.Add(2*time.Hour), &inboxID, &userID)
s.createEvent(1, "first_response", baseTime.Add(30*time.Minute), &inboxID, &userID)
s.createEvent(2, "first_response", baseTime.Add(2*time.Hour), &inboxID, &userID)
w := httptest.NewRecorder()
url := fmt.Sprintf(
"/api/v1/accounts/1/reporting_events?since=%d&until=%d&inbox_id=%d&user_id=%d&name=first_response",
baseTime.Add(time.Hour).Unix(), baseTime.Add(3*time.Hour).Unix(), inboxID, userID,
)
req := httptest.NewRequest(http.MethodGet, url, nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code, w.Body.String())
var resp map[string]any
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
payload := resp["payload"].([]any)
s.Len(payload, 1)
s.Equal(float64(matched.ID), payload[0].(map[string]any)["id"])
meta := resp["meta"].(map[string]any)
s.Equal(float64(1), meta["count"])
}
func (s *ReportingEventHandlerTestSuite) TestList_PaginatesAtChatwootFixedPageSize() {
baseTime := time.Now().Add(-40 * time.Hour).UTC()
for i := 0; i < 30; i++ {
s.createEvent(1, fmt.Sprintf("event_%02d", i), baseTime.Add(time.Duration(i)*time.Hour), nil, nil)
}
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events?page=2", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code, w.Body.String())
var resp map[string]any
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
payload := resp["payload"].([]any)
s.Len(payload, 5)
meta := resp["meta"].(map[string]any)
s.Equal(float64(30), meta["count"])
s.Equal(float64(2), meta["current_page"])
s.Equal(float64(2), meta["total_pages"])
}
func (s *ReportingEventHandlerTestSuite) TestList_InvalidFilters() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events?inbox_id=abc", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events?user_id=abc", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events?since=not-a-date&until=123", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *ReportingEventHandlerTestSuite) TestList_WithMetric() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z&metric=message_created", nil)
s.router.ServeHTTP(w, req)
// PG-specific SQL on SQLite may fail; accept both 200 and 500
code := w.Code
s.True(code == http.StatusOK || code == http.StatusUnprocessableEntity, "expected 200 or 500, got %d", code)
func (s *ReportingEventHandlerTestSuite) createEvent(accountID uint, name string, createdAt time.Time, inboxID, userID *uint) *model.ReportingEvent {
conversationID := uint(100 + createdAt.Unix()%1000)
event := &model.ReportingEvent{
Base: model.Base{CreatedAt: createdAt, UpdatedAt: createdAt},
AccountID: accountID,
Name: name,
Value: 12,
ValueInBusinessHours: 6,
ConversationID: &conversationID,
InboxID: inboxID,
UserID: userID,
EventStartTime: createdAt.Add(-time.Minute),
EventEndTime: createdAt,
}
s.Require().NoError(s.db.Create(event).Error)
return event
}
func (s *ReportingEventHandlerTestSuite) TestList_NoMetric() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reporting_events?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", nil)
s.router.ServeHTTP(w, req)
code := w.Code
s.True(code == http.StatusOK || code == http.StatusUnprocessableEntity, "expected 200 or 500, got %d", code)
}
@@ -13,6 +13,16 @@ type ReportingEventRepo struct {
db *gorm.DB
}
type ReportingEventListFilter struct {
Since *time.Time
Until *time.Time
InboxID uint
UserID uint
Name string
Offset int
Limit int
}
func NewReportingEventRepo(db *gorm.DB) *ReportingEventRepo {
return &ReportingEventRepo{db: db}
}
@@ -69,6 +79,31 @@ func (r *ReportingEventRepo) FindByConversationID(ctx context.Context, accountID
return events, err
}
func (r *ReportingEventRepo) FindByAccountFiltered(ctx context.Context, accountID uint, filter ReportingEventListFilter) ([]model.ReportingEvent, int64, error) {
query := r.db.WithContext(ctx).Model(&model.ReportingEvent{}).Where("account_id = ?", accountID)
if filter.Since != nil && filter.Until != nil {
query = query.Where("created_at >= ? AND created_at < ?", *filter.Since, *filter.Until)
}
if filter.InboxID != 0 {
query = query.Where("inbox_id = ?", filter.InboxID)
}
if filter.UserID != 0 {
query = query.Where("user_id = ?", filter.UserID)
}
if filter.Name != "" {
query = query.Where("name = ?", filter.Name)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var events []model.ReportingEvent
err := query.Order("created_at DESC").Offset(filter.Offset).Limit(filter.Limit).Find(&events).Error
return events, total, 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 {
+47 -1
View File
@@ -15,6 +15,23 @@ type ReportingEventService struct {
repo *repository.ReportingEventRepo
}
type ReportingEventListFilter struct {
Since *time.Time
Until *time.Time
InboxID uint
UserID uint
Name string
Page int
PerPage int
}
type ReportingEventListResult struct {
Events []model.ReportingEvent
Total int64
CurrentPage int
TotalPages int
}
// NewReportingEventService creates a new ReportingEvent service.
func NewReportingEventService(repo *repository.ReportingEventRepo) *ReportingEventService {
return &ReportingEventService{repo: repo}
@@ -40,6 +57,35 @@ func (s *ReportingEventService) GetByMetric(ctx context.Context, accountID uint,
return events, nil
}
func (s *ReportingEventService) ListAccountEvents(ctx context.Context, accountID uint, filter ReportingEventListFilter) (*ReportingEventListResult, error) {
if filter.Page < 1 {
filter.Page = 1
}
if filter.PerPage <= 0 {
filter.PerPage = 25
}
events, total, err := s.repo.FindByAccountFiltered(ctx, accountID, repository.ReportingEventListFilter{
Since: filter.Since,
Until: filter.Until,
InboxID: filter.InboxID,
UserID: filter.UserID,
Name: filter.Name,
Offset: (filter.Page - 1) * filter.PerPage,
Limit: filter.PerPage,
})
if err != nil {
applogger.L().Errorf("ReportingEventService.ListAccountEvents error: %v", err)
return nil, err
}
totalPages := 0
if total > 0 {
totalPages = int((total + int64(filter.PerPage) - 1) / int64(filter.PerPage))
}
return &ReportingEventListResult{Events: events, Total: total, CurrentPage: filter.Page, TotalPages: totalPages}, nil
}
// Create creates a new reporting event.
func (s *ReportingEventService) Create(ctx context.Context, event *model.ReportingEvent) error {
if err := s.repo.Create(ctx, event); err != nil {
@@ -47,4 +93,4 @@ func (s *ReportingEventService) Create(ctx context.Context, event *model.Reporti
return err
}
return nil
}
}