feat(reports): align outgoing message counts
This commit is contained in:
@@ -595,7 +595,7 @@ func (h *AnalyticsHandler) OutgoingMessagesCount(c *gin.Context) {
|
||||
}
|
||||
groupBy := c.Query("group_by")
|
||||
if groupBy != "agent" && groupBy != "team" && groupBy != "inbox" && groupBy != "label" {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrBadRequest, "invalid group_by")
|
||||
c.Status(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
|
||||
accounts.GET("/reports/conversations", s.handler.Conversations)
|
||||
accounts.GET("/reports/conversations_summary", s.handler.ConversationsSummary)
|
||||
accounts.GET("/reports/inbox_label_matrix", s.handler.InboxLabelMatrix)
|
||||
accounts.GET("/reports/outgoing_messages_count", s.handler.OutgoingMessagesCount)
|
||||
s.router = r
|
||||
}
|
||||
|
||||
@@ -504,6 +505,15 @@ func (s *AnalyticsHandlerTestSuite) TestInboxLabelMatrix_ParsesFrontendFilters()
|
||||
s.Require().Len(payload["labels"], 1)
|
||||
}
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestOutgoingMessagesCount_InvalidGroupByReturnsEmptyUnprocessableEntity() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reports/outgoing_messages_count?since=1735689600&until=1735776000&group_by=bad", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
||||
s.Empty(w.Body.String())
|
||||
}
|
||||
|
||||
// ========== Nil service guard ==========
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestNilService() {
|
||||
|
||||
@@ -212,6 +212,31 @@ func TestAnalyticsInboxLabelMatrixHonorsFiltersAndRange(t *testing.T) {
|
||||
require.Len(t, payload["labels"], 1)
|
||||
}
|
||||
|
||||
func TestAnalyticsOutgoingMessagesCountLabelUsesAccountLabelName(t *testing.T) {
|
||||
db, svc, account, inbox, contact, user, _ := setupAnalyticsP513Test(t)
|
||||
since := time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC)
|
||||
until := since.Add(24 * time.Hour)
|
||||
accountLabel := &model.Tag{AccountID: account.ID, Name: "vip"}
|
||||
require.NoError(t, db.Create(accountLabel).Error)
|
||||
otherAccount := &model.Account{Name: "Other"}
|
||||
require.NoError(t, db.Create(otherAccount).Error)
|
||||
foreignLabel := &model.Tag{AccountID: otherAccount.ID, Name: "vip"}
|
||||
require.NoError(t, db.Create(foreignLabel).Error)
|
||||
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, ChannelType: inbox.ChannelType, Channel: inbox.ChannelType}
|
||||
require.NoError(t, db.Create(conversation).Error)
|
||||
senderID := user.ID
|
||||
require.NoError(t, db.Create(&model.Message{Base: model.Base{CreatedAt: since.Add(time.Hour)}, AccountID: account.ID, InboxID: inbox.ID, ConversationID: conversation.ID, SenderID: &senderID, SenderType: "User", MessageType: string(model.MessageTypeOutgoing), Content: "hello"}).Error)
|
||||
require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: conversation.ID, TagID: foreignLabel.ID}).Error)
|
||||
|
||||
result, err := svc.GetOutgoingMessagesCountGrouped(context.Background(), account.ID, since, until, "label")
|
||||
require.NoError(t, err)
|
||||
rows := result.([]map[string]interface{})
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, accountLabel.ID, rows[0]["id"])
|
||||
assert.Equal(t, "vip", rows[0]["name"])
|
||||
assert.Equal(t, int64(1), rows[0]["outgoing_messages_count"])
|
||||
}
|
||||
|
||||
func TestAnalyticsTimeseriesAndRollupWorker(t *testing.T) {
|
||||
db, svc, account, inbox, contact, user, _ := setupAnalyticsP513Test(t)
|
||||
since := time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -979,15 +979,44 @@ func (s *AnalyticsService) outgoingMessagesByInbox(ctx context.Context, db *gorm
|
||||
}
|
||||
|
||||
func (s *AnalyticsService) outgoingMessagesByLabel(ctx context.Context, db *gorm.DB, accountID uint, since, until time.Time) ([]map[string]interface{}, error) {
|
||||
var rows []outgoingCountRow
|
||||
type labelRow struct {
|
||||
Name string
|
||||
Count int64
|
||||
}
|
||||
var rows []labelRow
|
||||
err := db.WithContext(ctx).Table("messages").
|
||||
Select("tags.id AS id, tags.name AS name, COUNT(*) AS count").
|
||||
Select("tags.name AS name, COUNT(*) AS count").
|
||||
Joins("INNER JOIN conversations ON conversations.id = messages.conversation_id").
|
||||
Joins("INNER JOIN conversation_labels ON conversation_labels.conversation_id = conversations.id").
|
||||
Joins("INNER JOIN tags ON tags.id = conversation_labels.tag_id").
|
||||
Where("messages.account_id = ? AND messages.message_type = ? AND messages.created_at >= ? AND messages.created_at < ?", accountID, string(model.MessageTypeOutgoing), since, until).
|
||||
Group("tags.id, tags.name").Scan(&rows).Error
|
||||
return outgoingRows(rows, "label"), err
|
||||
Group("tags.name").Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
names = append(names, row.Name)
|
||||
}
|
||||
var labels []model.Tag
|
||||
if len(names) > 0 {
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name IN ?", accountID, names).Find(&labels).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
labelIDs := map[string]uint{}
|
||||
for _, label := range labels {
|
||||
labelIDs[label.Name] = label.ID
|
||||
}
|
||||
result := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
var id interface{}
|
||||
if labelID, ok := labelIDs[row.Name]; ok {
|
||||
id = labelID
|
||||
}
|
||||
result = append(result, map[string]interface{}{"id": id, "name": row.Name, "outgoing_messages_count": row.Count})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func outgoingRows(rows []outgoingCountRow, _ string) []map[string]interface{} {
|
||||
|
||||
Reference in New Issue
Block a user