feat(reports): align v2 report envelopes
This commit is contained in:
@@ -49,11 +49,11 @@ Hermes task landing checklist:
|
||||
|
||||
## Current Baseline
|
||||
|
||||
- Current tracking checkpoint: 2026-06-06 profile MFA route/payload parity checkpoint, committed as `21421c6 feat(profile): align mfa payloads`.
|
||||
- Latest implementation checkpoint: `21421c6 feat(profile): align mfa payloads`.
|
||||
- Latest documentation/tooling checkpoint: this tracker landing update records the committed P3.39 baseline plus the landed parity tracker history; this document is the active follow-up plan and supersedes `.hermes/plans/*`.
|
||||
- Current tracking checkpoint: 2026-06-06 API v2 reports envelope/date-parameter parity checkpoint, prepared as `feat(reports): align v2 report envelopes`.
|
||||
- Latest implementation checkpoint: this checkpoint, prepared as `feat(reports): align v2 report envelopes`.
|
||||
- Latest documentation/tooling checkpoint: this tracker update records P3.40 API v2 report response/date parity plus the committed P3.39 baseline and 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: profile MFA routes from `reference/chatwoot/config/routes.rb:421-435`, `Api::V1::ProfilesController`, `Api::V1::Profile::MfaController`, MFA Jbuilder views, reused dashboard `mfa.js`, and profile MFA settings Vue flow are implemented for the reused frontend. GoChat now registers no-trailing and trailing `profile/mfa` route variants, tracks the profile route family, returns raw Chatwoot-shaped MFA status/create/verify/destroy/backup-code payloads, persists pending setup secrets before verification, returns backup codes after verification, stores backup code hashes in user custom attributes, validates password plus OTP/backup code for disable, and preserves the older `/api/v1/auth/mfa/*` compatibility endpoints. Facebook callbacks, account WhatsApp calls, onboarding, notification scoping, API v2 year-in-review, and WhatsApp calling toggle parity remain implemented from previous checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
|
||||
- Worktree status at this implementation checkpoint: API v2 reports from `reference/chatwoot/config/routes.rb:481-509`, `Api::V2::Accounts::ReportsController`, `SummaryReportsController`, `LiveReportsController`, and reused dashboard `reports.js`, `summaryReports.js`, and `liveReports.js` are aligned for frontend date params and raw JSON envelopes. Report and summary-report handlers now accept Chatwoot Unix-second `since/until` params while preserving RFC3339 compatibility, and API v2 reports/live_reports/summary_reports return raw JSON instead of the local `{ success, data }` envelope. Profile MFA, Facebook callbacks, account WhatsApp calls, onboarding, notification scoping, API v2 year-in-review, and WhatsApp calling toggle parity remain implemented from previous checkpoints. Live API/browser/enterprise smoke still needs the full PostgreSQL/Redis/Meilisearch/GoChat/Vite/Chrome stack.
|
||||
- Next executable implementation checkpoint: continue Phase 2/3 drift audit, Phase 6 placeholder audit, or B12 live smoke from fresh reference/smoke evidence.
|
||||
- `go test ./...` passes when run outside the restricted socket sandbox; focused Facebook callback handler/router/route-parity tests pass in the sandbox.
|
||||
- Route dump succeeds with `967` registered routes after profile MFA route tracking.
|
||||
@@ -140,6 +140,7 @@ This table is the shortest authoritative handoff view. If an older lower section
|
||||
|
||||
| Priority | Workstream | Current state | Next checkpoint | Commit close rule |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0 | P3.40 API v2 reports envelope/date parity | Implemented for reused dashboard reports clients: `GET /api/v2/accounts/:account_id/reports*`, `/summary_reports/*`, and `/live_reports/*` now follow the inspected Chatwoot v2 controllers by accepting Unix-second `since/until` params from `reports.js`/`summaryReports.js`, preserving existing RFC3339 compatibility for local callers, and returning raw JSON arrays/objects for reports, live reports, and summary reports instead of the local `{ success, data }` envelope. | Keep in Review; reopen from B12 reports smoke or fresh reference evidence for CSV body column drift, deeper metric-builder shape drift, authorization payload drift, or report CSV download content differences beyond this raw JSON/date-parameter slice. | Focused Analytics/SummaryReport/LiveReport handler tests, full `go test ./...`, and `git diff --check` must pass. No route artifacts change. |
|
||||
| 0 | P3.39 profile MFA route/payload parity | Implemented for reused dashboard profile MFA settings: `GET/PATCH/PUT /api/v1/profile`, profile action routes, and no-trailing `GET/POST/DELETE /api/v1/profile/mfa` plus `POST /profile/mfa/verify` and `POST /profile/mfa/backup_codes` are registered and tracked from `routes.rb:421-435`; profile MFA status returns raw `{ feature_available, enabled, backup_codes_generated }`; setup returns raw `{ provisioning_url, secret }` while persisting a pending TOTP secret; verify accepts frontend `otp_code`, enables MFA, and returns raw `{ enabled: true, backup_codes }`; backup-code regeneration validates the active OTP; destroy validates the current password plus OTP/backup-code and returns raw `{ enabled: false }`; legacy `/api/v1/auth/mfa/*` routes remain compatibility endpoints. | Keep in Review; reopen only if live profile security settings smoke exposes MFA feature-flag gating drift, backup-code storage shape drift, Devise password validation drift, or profile serializer fields beyond the inspected controller/Jbuilder/frontend contract. | Focused MFA handler/router/route-parity tests, route dump/parity regeneration (`967` routes; `421 exact`, `18 parameter-compatible`, `0 missing out of 439`), full `go test ./...`, and `git diff --check` must pass. |
|
||||
| 0 | P3.38 Facebook callbacks route/payload parity | Implemented for reused dashboard Facebook login and reauthorization flows: account-level `GET/POST /api/v1/accounts/:account_id/callbacks/register_facebook_page`, `POST /callbacks/facebook_pages`, frontend `.json` alias, and `POST /callbacks/reauthorize_page` are registered and tracked from `routes.rb:106-111`; page discovery exchanges `omniauth_token` for a long-lived user token through a fakeable Facebook provider boundary and returns `{ data: { page_details, user_access_token } }` with Chatwoot `exists` flags; register creates a Facebook inbox/channel from callback payloads and returns the raw callback Jbuilder fields; reauthorize validates account-scoped Facebook inboxes, refreshes the matching page token, clears `reauthorization_required`, updates inbox channel config, and returns `{ data: inbox }`. | Keep in Review; reopen only if live Facebook login smoke exposes Koala pagination/body drift, avatar import side-effect requirements, Facebook page subscription setup drift, or inbox serializer fields beyond the inspected callbacks controller/Jbuilder/frontend contract. | Focused Facebook callback handler tests, router/route-parity tests, route dump/parity regeneration (`963` routes; `407 exact`, `18 parameter-compatible`, `0 missing out of 425`), full `go test ./...`, and `git diff --check` must pass. |
|
||||
| 0 | P3.37 account WhatsApp calls API parity | Implemented for reused dashboard WhatsApp WebRTC call controls: account-level `GET /api/v1/accounts/:account_id/whatsapp_calls/:id`, `POST /whatsapp_calls/initiate`, `POST /whatsapp_calls/:id/accept`, `reject`, `terminate`, and `upload_recording` are registered and tracked from `routes.rb:237-245`; initiate validates enabled WhatsApp Cloud calling, `sdp_offer`, and contact phone; outbound calls persist as Chatwoot `calls` with linked `voice_call` messages and return `{ status: "calling", call_id }`; accept/reject/terminate update call/message/conversation status through a fakeable Meta provider boundary; permission-request and upload-recording idempotency responses match the inspected controller/spec contract. | Keep in Review; reopen only if live WhatsApp calling smoke exposes Meta call action body drift, ActionCable broadcast requirements beyond persisted state, permission-template throttling drift, recording storage URL drift, or call bubble serializer drift beyond the inspected enterprise controller/service/Jbuilder/frontend contract. | Focused WhatsApp call service/handler tests, router/route-parity tests, route dump/parity regeneration (`958` routes; `403 exact`, `18 parameter-compatible`, `0 missing out of 421`), full `go test ./...`, and `git diff --check` must pass. |
|
||||
@@ -262,6 +263,7 @@ This ledger records the committed parity checkpoints that future slices should b
|
||||
|
||||
| Commit | Scope | Verification summary | Follow-up state |
|
||||
| --- | --- | --- | --- |
|
||||
| `feat(reports): align v2 report envelopes` | Advances P3.40 API v2 reports envelope/date-parameter parity by matching Chatwoot `Api::V2::Accounts::ReportsController`, `SummaryReportsController`, `LiveReportsController`, routes `481-509`, and reused dashboard `reports.js`, `summaryReports.js`, and `liveReports.js`. GoChat now accepts Unix-second `since/until` report params from the frontend while preserving RFC3339 compatibility, and returns raw Chatwoot-style JSON arrays/objects for reports, summary reports, and live reports instead of the local `{ success, data }` envelope. | `go test ./internal/handler/api/v1 -run 'Analytics\|SummaryReport\|LiveReport' -count=1`; full `go test ./...`; `git diff --check`. Route artifacts are unchanged. | P3.40 moves to Review for the current v2 report JSON/date evidence; continue Phase 2/3 drift audit for CSV/metric-builder depth, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. |
|
||||
| `feat(profile): align mfa payloads` | Advances P3.39 profile MFA route/payload parity by matching Chatwoot `Api::V1::ProfilesController`, `Api::V1::Profile::MfaController`, MFA Jbuilder views, routes `421-435`, and reused dashboard `mfa.js`/profile MFA settings flow. GoChat now exposes no-trailing profile MFA routes, tracks profile route parity, returns raw MFA status/create/verify/destroy/backup-code payloads, persists pending setup secrets, verifies frontend `otp_code`, returns and stores backup-code hashes, and validates password plus OTP/backup-code for disable while preserving legacy `/auth/mfa` compatibility endpoints. | `go test ./internal/auth ./internal/handler/api/v1 ./internal/router ./cmd/route_parity -run 'MFA\|Profile\|RegisterRoutes\|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: 967`; tracked route parity is `421 exact, 0 method-compatible, 18 parameter-compatible, 0 missing out of 439`. | P3.39 moves to Review for current profile MFA evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. |
|
||||
| `feat(channels): align facebook callbacks` | Advances P3.38 Facebook callbacks route/payload parity by matching Chatwoot `Api::V1::Accounts::CallbacksController`, callback Jbuilder views/specs, routes `106-111`, and reused dashboard `fbChannel.js`/`endPoints.js`/Facebook reauthorize store flow. GoChat now exposes account-level register/page-discovery/reauthorize callback routes, supports the frontend `.json` page-discovery alias, exchanges short-lived login tokens through a fakeable provider boundary, marks existing Facebook pages, creates Facebook inboxes from callback payloads, persists `user_access_token`, refreshes page tokens and inbox channel config on reauthorize, and returns Chatwoot-shaped raw callback payloads. | `go test ./internal/handler/api/v1 ./internal/channel/facebook ./internal/router ./cmd/route_parity -run 'Facebook\|RegisterRoutes\|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: 963`; tracked route parity is `407 exact, 0 method-compatible, 18 parameter-compatible, 0 missing out of 425`. | P3.38 moves to Review for current Facebook callback evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. |
|
||||
| `feat(channels): align whatsapp calls api` | Advances P3.37 account WhatsApp calls API parity by matching Chatwoot enterprise `Api::V1::Accounts::WhatsappCallsController`, `Whatsapp::CallService`, `Voice::CallMessageBuilder`, `Call`, WhatsApp call Jbuilder views/specs, route `237-245`, and reused dashboard `whatsappCallsAPI.js`. GoChat now exposes account-level show/initiate/accept/reject/terminate/upload-recording routes, persists WhatsApp calls through the `calls` model boundary, creates linked `voice_call` messages, updates call/message/conversation status for lifecycle actions, records permission-request attributes, and makes recording upload idempotent. | `go test ./internal/service ./internal/handler/api/v1 ./internal/router ./cmd/route_parity -run 'WhatsAppCall\|RegisterRoutes\|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: 958`; tracked route parity is `403 exact, 0 method-compatible, 18 parameter-compatible, 0 missing out of 421`. | P3.37 moves to Review for current WhatsApp calls evidence; continue Phase 2/3 drift audit, Phase 6 placeholder audit, B12 live smoke, or fresh reference/smoke drift. |
|
||||
@@ -2509,3 +2511,4 @@ Verification milestone gates:
|
||||
- 2026-06-06: P3.38 Facebook callbacks checkpoint committed as `6e66af7 feat(channels): align facebook callbacks`; audited Chatwoot account callbacks controller/views/specs, routes `106-111`, and reused dashboard Facebook login/reauthorization clients. GoChat now exposes account-level page discovery/register/reauthorize callbacks, supports the frontend `.json` alias, exchanges tokens through a fakeable Facebook provider boundary, marks existing pages, creates Facebook inboxes, refreshes page tokens, and returns raw Chatwoot callback payloads. Focused Facebook handler/channel/router/route-parity tests, route dump/parity regeneration (`TOTAL: 963`; `407 exact`, `18 parameter-compatible`, `0 missing out of 425`), full `go test ./...`, and `git diff --check` passed.
|
||||
- 2026-06-06: P3.39 profile MFA checkpoint committed as `21421c6 feat(profile): align mfa payloads`; audited Chatwoot profile/profile-MFA controllers, MFA Jbuilder views, routes `421-435`, reused dashboard `mfa.js`, and profile MFA settings flow. GoChat now tracks profile routes, registers no-trailing and trailing MFA route variants, returns raw MFA status/create/verify/destroy/backup-code payloads, persists pending setup secrets, stores backup-code hashes, validates password plus OTP/backup-code for disable, and preserves legacy `/api/v1/auth/mfa/*` compatibility endpoints. Focused MFA/profile/router/route-parity tests, route dump/parity regeneration (`TOTAL: 967`; `421 exact`, `18 parameter-compatible`, `0 missing out of 439`), full `go test ./...`, and `git diff --check` passed.
|
||||
- 2026-06-06: Documentation checkpoint prepared as `docs: land parity execution tracker`; confirms the clean committed baseline at `21421c6 feat(profile): align mfa payloads`, makes `docs/CHATWOOT_PARITY_DEVELOPMENT_PLAN.md` the self-contained active tracker for the user-confirmed scope, marks `.hermes/plans/*` as source notes only, replaces stale next-slice wording with the Phase 2/3 audit, Phase 6 placeholder audit, and B12 live-smoke queue, and records the missing P3.35-P3.39 progress entries. Verification for this docs-only checkpoint: `git diff --check`.
|
||||
- 2026-06-06: P3.40 API v2 reports checkpoint prepared as `feat(reports): align v2 report envelopes`; audited Chatwoot v2 reports, summary reports, and live reports controllers plus reused dashboard `reports.js`, `summaryReports.js`, and `liveReports.js`. GoChat report handlers now accept frontend Unix-second `since/until` params while preserving RFC3339 compatibility, and v2 reports/live_reports/summary_reports return raw Chatwoot JSON arrays/objects instead of the local `{ success, data }` envelope. Focused Analytics/SummaryReport/LiveReport handler tests, escalated full `go test ./...`, and `git diff --check` passed. No route artifacts change.
|
||||
|
||||
@@ -54,7 +54,7 @@ func (h *AnalyticsHandler) Index(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate report")
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// parseAccountID extracts account_id from URL params.
|
||||
@@ -77,13 +77,13 @@ func parseDateRange(c *gin.Context) (since, until time.Time, ok bool) {
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
since, err := time.Parse(time.RFC3339, sinceStr)
|
||||
since, err := parseChatwootReportTime(sinceStr)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format")
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
until, err = time.Parse(time.RFC3339, untilStr)
|
||||
until, err = parseChatwootReportTime(untilStr)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format")
|
||||
return time.Time{}, time.Time{}, false
|
||||
@@ -92,6 +92,14 @@ func parseDateRange(c *gin.Context) (since, until time.Time, ok bool) {
|
||||
return since, until, true
|
||||
}
|
||||
|
||||
func parseChatwootReportTime(value string) (time.Time, error) {
|
||||
if unixSeconds, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return time.Unix(unixSeconds, 0).UTC(), nil
|
||||
}
|
||||
|
||||
return time.Parse(time.RFC3339, value)
|
||||
}
|
||||
|
||||
// Summary returns account-level aggregated metrics.
|
||||
// GET /api/v1/accounts/:account_id/reports/summary
|
||||
func (h *AnalyticsHandler) Summary(c *gin.Context) {
|
||||
@@ -111,7 +119,7 @@ func (h *AnalyticsHandler) Summary(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// AgentMetrics returns metrics grouped by agent.
|
||||
@@ -133,7 +141,7 @@ func (h *AnalyticsHandler) AgentMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// InboxMetrics returns metrics grouped by inbox.
|
||||
@@ -155,7 +163,7 @@ func (h *AnalyticsHandler) InboxMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// LabelMetrics returns metrics grouped by label.
|
||||
@@ -177,7 +185,7 @@ func (h *AnalyticsHandler) LabelMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// TeamMetrics returns metrics grouped by team.
|
||||
@@ -199,7 +207,7 @@ func (h *AnalyticsHandler) TeamMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ConversationTraffic returns daily conversation traffic time-series.
|
||||
@@ -221,7 +229,7 @@ func (h *AnalyticsHandler) ConversationTraffic(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// BotSummary returns bot-level summary metrics.
|
||||
@@ -244,7 +252,7 @@ func (h *AnalyticsHandler) BotSummary(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Conversations returns conversation metrics filtered by type.
|
||||
@@ -270,7 +278,7 @@ func (h *AnalyticsHandler) Conversations(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ConversationsSummary returns conversation summary report.
|
||||
@@ -293,7 +301,7 @@ func (h *AnalyticsHandler) ConversationsSummary(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// BotMetrics returns bot metrics.
|
||||
@@ -316,7 +324,7 @@ func (h *AnalyticsHandler) BotMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// InboxLabelMatrix returns inbox-label matrix data.
|
||||
@@ -335,7 +343,7 @@ func (h *AnalyticsHandler) InboxLabelMatrix(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// FirstResponseTimeDistribution returns first response time distribution.
|
||||
@@ -358,7 +366,7 @@ func (h *AnalyticsHandler) FirstResponseTimeDistribution(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// OutgoingMessagesCount returns outgoing message count metrics.
|
||||
@@ -386,5 +394,5 @@ func (h *AnalyticsHandler) OutgoingMessagesCount(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -123,15 +123,14 @@ func (s *AnalyticsHandlerTestSuite) TestSummary_InvalidUntilFormat() {
|
||||
func (s *AnalyticsHandlerTestSuite) TestSummary_EmptyData() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet,
|
||||
"/api/v1/accounts/1/reports/summary?since=2025-01-01T00:00:00Z&until=2025-01-31T00:00:00Z", nil)
|
||||
"/api/v1/accounts/1/reports/summary?since=1735689600&until=1738281600", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
s.True(body["success"].(bool))
|
||||
data := body["data"].(map[string]interface{})
|
||||
s.NotNil(data["metrics"])
|
||||
s.NotContains(body, "success")
|
||||
s.NotNil(body["metrics"])
|
||||
}
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestSummary_WithData() {
|
||||
@@ -160,12 +159,11 @@ func (s *AnalyticsHandlerTestSuite) TestIndex_TimeseriesWithData() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet,
|
||||
"/api/v1/accounts/1/reports?metric=conversations_count&since=2025-01-01T00:00:00Z&until=2025-02-01T00:00:00Z&type=account&group_by=day", nil)
|
||||
"/api/v1/accounts/1/reports?metric=conversations_count&since=1735689600&until=1738368000&type=account&group_by=day", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
data := body["data"].([]interface{})
|
||||
var data []interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &data))
|
||||
s.Len(data, 1)
|
||||
s.Equal(float64(1), data[0].(map[string]interface{})["value"])
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (h *LiveReportHandler) ConversationMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GroupedConversationMetrics returns conversation metrics grouped by team_id or assignee_id.
|
||||
@@ -76,5 +76,5 @@ func (h *LiveReportHandler) GroupedConversationMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -50,9 +51,15 @@ func (s *LiveReportHandlerTestSuite) TestConversationMetrics_InvalidAccountID()
|
||||
}
|
||||
|
||||
func (s *LiveReportHandlerTestSuite) TestConversationMetrics_Success() {
|
||||
// Placeholder implementation returns 200 with zero counts
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/live_reports/conversation_metrics", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
}
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
s.NotContains(body, "success")
|
||||
s.Equal(float64(0), body["open"])
|
||||
s.Equal(float64(0), body["unattended"])
|
||||
s.Equal(float64(0), body["unassigned"])
|
||||
s.Equal(float64(0), body["pending"])
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (h *SummaryReportHandler) Agent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Team returns team-level summary metrics.
|
||||
@@ -68,7 +68,7 @@ func (h *SummaryReportHandler) Team(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Inbox returns inbox-level summary metrics.
|
||||
@@ -90,7 +90,7 @@ func (h *SummaryReportHandler) Inbox(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Label returns label-level summary metrics.
|
||||
@@ -112,7 +112,7 @@ func (h *SummaryReportHandler) Label(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Channel returns channel-level summary metrics.
|
||||
@@ -144,7 +144,7 @@ func (h *SummaryReportHandler) Channel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// parseSummaryDateRange extracts since/until from query params for summary reports.
|
||||
@@ -157,15 +157,15 @@ func parseSummaryDateRange(c *gin.Context) (since, until time.Time, ok bool) {
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
since, err := time.Parse(time.RFC3339, sinceStr)
|
||||
since, err := parseChatwootReportTime(sinceStr)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format (use RFC3339)")
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format")
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
until, err = time.Parse(time.RFC3339, untilStr)
|
||||
until, err = parseChatwootReportTime(untilStr)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format (use RFC3339)")
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format")
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -39,6 +40,7 @@ func (s *SummaryReportHandlerTestSuite) SetupSuite() {
|
||||
accountGroup.GET("/summary_reports/team", s.handler.Team)
|
||||
accountGroup.GET("/summary_reports/inbox", s.handler.Inbox)
|
||||
accountGroup.GET("/summary_reports/label", s.handler.Label)
|
||||
accountGroup.GET("/summary_reports/channel", s.handler.Channel)
|
||||
s.router = r
|
||||
|
||||
s.account = &model.Account{Name: "TestAccount"}
|
||||
@@ -51,7 +53,7 @@ func TestSummaryReportHandlerTestSuite(t *testing.T) {
|
||||
|
||||
func (s *SummaryReportHandlerTestSuite) TestAgent_InvalidAccountID() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/agent?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/agent?since=1704067200&until=1735689599", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
}
|
||||
@@ -65,15 +67,16 @@ func (s *SummaryReportHandlerTestSuite) TestAgent_MissingDateRange() {
|
||||
|
||||
func (s *SummaryReportHandlerTestSuite) TestAgent_Success() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/summary_reports/agent?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", s.account.ID), nil)
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/summary_reports/agent?since=1704067200&until=1735689599", s.account.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
// PG-specific SQL may fail on SQLite
|
||||
s.True(w.Code == http.StatusOK || w.Code == http.StatusUnprocessableEntity, "got %d", w.Code)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
var body []interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
}
|
||||
|
||||
func (s *SummaryReportHandlerTestSuite) TestTeam_InvalidAccountID() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/team?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/team?since=1704067200&until=1735689599", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
}
|
||||
@@ -87,14 +90,21 @@ func (s *SummaryReportHandlerTestSuite) TestTeam_MissingDateRange() {
|
||||
|
||||
func (s *SummaryReportHandlerTestSuite) TestInbox_InvalidAccountID() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/inbox?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/inbox?since=1704067200&until=1735689599", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
func (s *SummaryReportHandlerTestSuite) TestLabel_InvalidAccountID() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/label?since=2024-01-01T00:00:00Z&until=2024-12-31T23:59:59Z", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/summary_reports/label?since=1704067200&until=1735689599", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SummaryReportHandlerTestSuite) TestChannel_DateRangeTooLong() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/%d/summary_reports/channel?since=1704067200&until=1735689599", s.account.ID), nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user