feat(reports): align v2 report envelopes
This commit is contained in:
@@ -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