feat(reports): align conversation agent metrics

This commit is contained in:
2026-06-07 01:15:25 +08:00
parent 18f6403128
commit 108aae44b2
6 changed files with 171 additions and 17 deletions
+8 -2
View File
@@ -416,11 +416,17 @@ func (h *AnalyticsHandler) Conversations(c *gin.Context) {
reportType := c.Query("type")
if reportType == "" {
// Reference: Chatwoot returns head :unprocessable_entity if type is blank
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrBadRequest, "type parameter is required")
c.Status(http.StatusUnprocessableEntity)
return
}
page := 1
if rawPage := c.Query("page"); rawPage != "" {
if parsed, err := strconv.Atoi(rawPage); err == nil && parsed > 0 {
page = parsed
}
}
result, err := h.svc.GetConversationsByType(c.Request.Context(), accountID, reportType, time.Time{}, time.Time{})
result, err := h.svc.GetConversationsByType(c.Request.Context(), accountID, reportType, page)
if err != nil {
applogger.L().Errorf("Conversations report: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate conversations report")
@@ -40,6 +40,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
s.Require().NoError(db.AutoMigrate(
&model.Account{},
&model.User{},
&model.AccountUser{},
&model.Inbox{},
&model.Team{},
&model.Tag{},
@@ -70,6 +71,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
accounts.GET("/reports/labels", s.handler.LabelMetrics)
accounts.GET("/reports/teams", s.handler.TeamMetrics)
accounts.GET("/reports/conversation_traffic", s.handler.ConversationTraffic)
accounts.GET("/reports/conversations", s.handler.Conversations)
accounts.GET("/reports/conversations_summary", s.handler.ConversationsSummary)
s.router = r
}
@@ -88,6 +90,7 @@ func (s *AnalyticsHandlerTestSuite) SetupTest() {
s.db.Exec("DELETE FROM tags")
s.db.Exec("DELETE FROM teams")
s.db.Exec("DELETE FROM inboxes")
s.db.Exec("DELETE FROM account_users")
s.db.Exec("DELETE FROM users")
}
@@ -416,6 +419,56 @@ func (s *AnalyticsHandlerTestSuite) TestConversationsSummary_ReturnsCSVDownload(
s.Equal("1", rows[2][2])
}
func (s *AnalyticsHandlerTestSuite) TestConversations_MissingTypeReturnsEmptyUnprocessableEntity() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reports/conversations", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusUnprocessableEntity, w.Code)
s.Empty(w.Body.String())
}
func (s *AnalyticsHandlerTestSuite) TestConversations_AgentTypeReturnsChatwootAgentMetrics() {
agentOne := model.User{AccountID: s.accountID, Name: "Low", Email: "low@example.com", Password: "secret", AvatarURL: "https://example.com/low.png", Active: true}
agentTwo := model.User{AccountID: s.accountID, Name: "High", Email: "high@example.com", Password: "secret", Active: true}
s.Require().NoError(s.db.Create(&agentOne).Error)
s.Require().NoError(s.db.Create(&agentTwo).Error)
s.Require().NoError(s.db.Create(&model.AccountUser{AccountID: s.accountID, UserID: agentOne.ID, Role: "agent", Availability: "offline"}).Error)
s.Require().NoError(s.db.Create(&model.AccountUser{AccountID: s.accountID, UserID: agentTwo.ID, Role: "agent", Availability: "online"}).Error)
inbox := model.Inbox{AccountID: s.accountID, Name: "Web", ChannelType: "web_widget", ChannelID: 1, Enabled: true}
s.Require().NoError(s.db.Create(&inbox).Error)
lowFirstReply := int64(1735689700)
for i := 0; i < 2; i++ {
conv := model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: 1, AssigneeID: &agentTwo.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
s.Require().NoError(s.db.Create(&conv).Error)
}
lowConv := model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: 1, AssigneeID: &agentOne.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", FirstReplyCreatedAt: &lowFirstReply}
s.Require().NoError(s.db.Create(&lowConv).Error)
pendingForHigh := model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: 1, AssigneeID: &agentTwo.ID, Status: string(model.ConversationStatusPending), ChannelType: "web_widget", Channel: "web_widget"}
s.Require().NoError(s.db.Create(&pendingForHigh).Error)
unassigned := model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
s.Require().NoError(s.db.Create(&unassigned).Error)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reports/conversations?type=agent&page=1", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var payload []map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
s.Require().Len(payload, 2)
s.Equal("High", payload[0]["name"])
s.Equal("high@example.com", payload[0]["email"])
s.Equal("online", payload[0]["availability"])
metric := payload[0]["metric"].(map[string]interface{})
s.Equal(float64(2), metric["open"])
s.Equal(float64(2), metric["unattended"])
s.NotContains(metric, "unassigned")
s.NotContains(metric, "pending")
s.Equal("Low", payload[1]["name"])
s.Equal("https://example.com/low.png", payload[1]["thumbnail"])
}
// ========== Nil service guard ==========
func (s *AnalyticsHandlerTestSuite) TestNilService() {