193 lines
10 KiB
Go
193 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/worker"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupAnalyticsP513Test(t *testing.T) (*gorm.DB, *AnalyticsService, *model.Account, *model.Inbox, *model.Contact, *model.User, *model.Team) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.AccountUser{},
|
|
&model.Team{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.Message{},
|
|
&model.ReportingEvent{},
|
|
&model.ReportingEventsRollup{},
|
|
&model.AgentBot{},
|
|
&model.AgentBotInbox{},
|
|
&model.Tag{},
|
|
&model.ConversationLabel{},
|
|
&model.BackgroundJob{},
|
|
))
|
|
t.Cleanup(func() {
|
|
sqlDB, _ := db.DB()
|
|
sqlDB.Close()
|
|
})
|
|
account := &model.Account{Name: "Analytics"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
user := &model.User{AccountID: account.ID, Name: "Agent", Email: "agent@example.com", Password: "secret", Active: true}
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: string(model.AccountUserRoleAgent)}).Error)
|
|
team := &model.Team{AccountID: account.ID, Name: "Support"}
|
|
require.NoError(t, db.Create(team).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Web", ChannelType: "web_widget", ChannelID: 1, Enabled: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Customer", Email: "customer@example.com"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
return db, svc, account, inbox, contact, user, team
|
|
}
|
|
|
|
func TestAnalyticsLiveConversationMetricsAreDerivedFromConversations(t *testing.T) {
|
|
db, svc, account, inbox, contact, user, team := setupAnalyticsP513Test(t)
|
|
now := int64(1760000000)
|
|
firstReply := now - 60
|
|
assigned := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, TeamID: &team.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType}
|
|
unassigned := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, TeamID: &team.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType, FirstReplyCreatedAt: &firstReply}
|
|
pending := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusPending), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType}
|
|
otherAccount := &model.Account{Name: "Other"}
|
|
require.NoError(t, db.Create(otherAccount).Error)
|
|
other := &model.Conversation{AccountID: otherAccount.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType}
|
|
require.NoError(t, db.Create(assigned).Error)
|
|
require.NoError(t, db.Create(unassigned).Error)
|
|
require.NoError(t, db.Create(pending).Error)
|
|
require.NoError(t, db.Create(other).Error)
|
|
|
|
metrics, err := svc.GetConversationMetrics(context.Background(), account.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), metrics.OpenCount)
|
|
assert.Equal(t, int64(1), metrics.UnattendedCount)
|
|
assert.Equal(t, int64(1), metrics.UnassignedCount)
|
|
assert.Equal(t, int64(1), metrics.PendingCount)
|
|
|
|
teamMetrics, err := svc.GetConversationMetricsForTeam(context.Background(), account.ID, team.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), teamMetrics.OpenCount)
|
|
assert.Equal(t, int64(0), teamMetrics.PendingCount)
|
|
|
|
grouped, err := svc.GetGroupedConversationMetrics(context.Background(), account.ID, "assignee_id")
|
|
require.NoError(t, err)
|
|
require.Len(t, grouped, 2)
|
|
assert.Equal(t, int64(1), grouped[0]["open"])
|
|
assert.Nil(t, grouped[0]["assignee_id"])
|
|
assert.Equal(t, user.ID, grouped[1]["assignee_id"])
|
|
assert.Equal(t, int64(1), grouped[1]["open"])
|
|
}
|
|
|
|
func TestAnalyticsReportsUsePersistedConversationMessageAndEventRows(t *testing.T) {
|
|
db, svc, account, inbox, contact, user, team := setupAnalyticsP513Test(t)
|
|
since := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
|
|
until := since.Add(24 * time.Hour)
|
|
resolvedAt := since.Add(3 * time.Hour)
|
|
conv := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, TeamID: &team.ID, Status: string(model.ConversationStatusResolved), ResolvedAt: &resolvedAt, ChannelType: inbox.ChannelType, Channel: inbox.ChannelType}
|
|
require.NoError(t, db.Create(conv).Error)
|
|
require.NoError(t, db.Model(conv).Updates(map[string]interface{}{"created_at": since.Add(time.Hour)}).Error)
|
|
senderID := user.ID
|
|
require.NoError(t, db.Create(&model.Message{Base: model.Base{CreatedAt: since.Add(2 * time.Hour)}, AccountID: account.ID, ConversationID: conv.ID, InboxID: inbox.ID, SenderID: &senderID, SenderType: "User", MessageType: string(model.MessageTypeOutgoing), Content: "hello"}).Error)
|
|
require.NoError(t, db.Create(&model.Message{Base: model.Base{CreatedAt: since.Add(90 * time.Minute)}, AccountID: account.ID, ConversationID: conv.ID, InboxID: inbox.ID, MessageType: string(model.MessageTypeIncoming), Content: "hi"}).Error)
|
|
bot := &model.AgentBot{AccountID: &account.ID, Name: "Bot"}
|
|
require.NoError(t, db.Create(bot).Error)
|
|
require.NoError(t, db.Create(&model.AgentBotInbox{AccountID: &account.ID, AgentBotID: bot.ID, InboxID: inbox.ID, Status: model.AgentBotInboxActive}).Error)
|
|
tag := &model.Tag{AccountID: account.ID, Name: "vip"}
|
|
require.NoError(t, db.Create(tag).Error)
|
|
require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: conv.ID, TagID: tag.ID}).Error)
|
|
require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: since.Add(30 * time.Minute)}, AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 1800, ConversationID: &conv.ID, InboxID: &inbox.ID, UserID: &user.ID, EventStartTime: since, EventEndTime: since.Add(30 * time.Minute)}).Error)
|
|
require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: since.Add(time.Hour)}, AccountID: account.ID, Name: "conversation_bot_resolved", Value: 1, ConversationID: &conv.ID, InboxID: &inbox.ID, EventStartTime: since, EventEndTime: since.Add(time.Hour)}).Error)
|
|
|
|
summary, err := svc.GetConversationsSummary(context.Background(), account.ID, since, until)
|
|
require.NoError(t, err)
|
|
summaryMap := summary.(map[string]interface{})
|
|
assert.Equal(t, int64(1), summaryMap["conversations_count"])
|
|
assert.Equal(t, int64(1), summaryMap["incoming_messages_count"])
|
|
assert.Equal(t, int64(1), summaryMap["outgoing_messages_count"])
|
|
assert.Equal(t, int64(1), summaryMap["resolutions_count"])
|
|
assert.Equal(t, 1800.0, summaryMap["avg_first_response_time"])
|
|
|
|
botSummary, err := svc.GetBotSummary(context.Background(), account.ID, since, until)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), botSummary.BotResolutionsCount)
|
|
assert.Equal(t, int64(0), botSummary.BotHandoffsCount)
|
|
assert.NotNil(t, botSummary.Previous)
|
|
|
|
botMetrics, err := svc.GetBotMetrics(context.Background(), account.ID, since, until)
|
|
require.NoError(t, err)
|
|
botMap := botMetrics.(map[string]interface{})
|
|
assert.Equal(t, int64(1), botMap["conversation_count"])
|
|
assert.Equal(t, int64(1), botMap["message_count"])
|
|
assert.Equal(t, 100, botMap["resolution_rate"])
|
|
|
|
distribution, err := svc.GetFirstResponseTimeDistribution(context.Background(), account.ID, since, until)
|
|
require.NoError(t, err)
|
|
distMap := distribution.(map[string]map[string]int64)
|
|
assert.Equal(t, int64(1), distMap["web_widget"]["0-1h"])
|
|
|
|
outgoing, err := svc.GetOutgoingMessagesCountGrouped(context.Background(), account.ID, since, until, "inbox")
|
|
require.NoError(t, err)
|
|
outRows := outgoing.([]map[string]interface{})
|
|
require.Len(t, outRows, 1)
|
|
assert.Equal(t, inbox.ID, outRows[0]["id"])
|
|
assert.Equal(t, int64(1), outRows[0]["outgoing_messages_count"])
|
|
|
|
matrix, err := svc.GetInboxLabelMatrix(context.Background(), account.ID)
|
|
require.NoError(t, err)
|
|
matrixMap := matrix.(map[string]interface{})
|
|
assert.Equal(t, [][]int64{{1}}, matrixMap["matrix"])
|
|
}
|
|
|
|
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)
|
|
until := since.Add(48 * time.Hour)
|
|
conv := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType}
|
|
require.NoError(t, db.Create(conv).Error)
|
|
require.NoError(t, db.Model(conv).Updates(map[string]interface{}{"created_at": since.Add(2 * time.Hour)}).Error)
|
|
require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: since.Add(3 * time.Hour)}, AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 120, ConversationID: &conv.ID, InboxID: &inbox.ID, UserID: &user.ID, EventStartTime: since, EventEndTime: since.Add(2 * time.Minute)}).Error)
|
|
|
|
points, err := svc.GetTimeseries(context.Background(), account.ID, "conversations_count", since, until, "account", 0, "day", false)
|
|
require.NoError(t, err)
|
|
require.Len(t, points, 1)
|
|
assert.Equal(t, float64(1), points[0].Value)
|
|
assert.Equal(t, since.Unix(), points[0].Timestamp)
|
|
|
|
avgPoints, err := svc.GetTimeseries(context.Background(), account.ID, "avg_first_response_time", since, until, "agent", user.ID, "day", false)
|
|
require.NoError(t, err)
|
|
require.Len(t, avgPoints, 1)
|
|
assert.Equal(t, 120.0, avgPoints[0].Value)
|
|
assert.Equal(t, int64(1), avgPoints[0].Count)
|
|
|
|
wp := worker.NewWorkerPool(db)
|
|
svc.SetWorkerPool(wp)
|
|
job, err := EnqueueReportingRollupDay(context.Background(), wp, account.ID, since)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, job)
|
|
again, err := EnqueueReportingRollupDay(context.Background(), wp, account.ID, since)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, job.ID, again.ID)
|
|
processed, err := wp.ProcessOne(context.Background())
|
|
require.NoError(t, err)
|
|
assert.True(t, processed)
|
|
processed, err = wp.ProcessOne(context.Background())
|
|
require.NoError(t, err)
|
|
assert.False(t, processed)
|
|
|
|
var rollupCount int64
|
|
require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Where("account_id = ? AND date = ?", account.ID, since).Count(&rollupCount).Error)
|
|
assert.Greater(t, rollupCount, int64(0))
|
|
}
|