package service import ( "context" "testing" "time" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupReportingRollupTZTest(t *testing.T) (*gorm.DB, *ReportingRollupService, *ReportingBackfillService) { 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.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.ReportingEvent{}, &model.ReportingEventsRollup{}, )) t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) eventRepo := repository.NewReportingEventRepo(db) rollupRepo := repository.NewReportingEventsRollupRepo(db) return db, NewReportingRollupService(eventRepo, rollupRepo), NewReportingBackfillService(eventRepo, rollupRepo) } func createReportingAccount(t *testing.T, db *gorm.DB, reportingTimezone string) *model.Account { t.Helper() account := &model.Account{Name: "Reports", ReportingTimezone: reportingTimezone} require.NoError(t, db.Create(account).Error) return account } func createReportingActorRows(t *testing.T, db *gorm.DB, accountID uint) (*model.User, *model.Inbox, *model.Conversation) { t.Helper() user := &model.User{AccountID: accountID, Name: "Agent", Email: "agent@example.com", Password: "secret", Active: true} require.NoError(t, db.Create(user).Error) inbox := &model.Inbox{AccountID: accountID, Name: "Web", ChannelType: "web_widget", ChannelID: 1, Enabled: true} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: accountID, Name: "Customer"} require.NoError(t, db.Create(contact).Error) conversation := &model.Conversation{AccountID: accountID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} require.NoError(t, db.Create(conversation).Error) return user, inbox, conversation } func createReportingEvent(t *testing.T, db *gorm.DB, accountID uint, name string, createdAt time.Time, userID, inboxID, conversationID *uint) { t.Helper() require.NoError(t, db.Create(&model.ReportingEvent{ Base: model.Base{CreatedAt: createdAt}, AccountID: accountID, Name: name, Value: 100, ValueInBusinessHours: 50, UserID: userID, InboxID: inboxID, ConversationID: conversationID, EventStartTime: createdAt.AddDate(0, 0, -3), EventEndTime: createdAt.AddDate(0, 0, -3).Add(time.Minute), }).Error) } func TestReportingRollupSkipsBlankAndInvalidReportingTimezone(t *testing.T) { db, rollups, _ := setupReportingRollupTZTest(t) blank := createReportingAccount(t, db, "") invalid := createReportingAccount(t, db, "Invalid/Timezone") createReportingEvent(t, db, blank.ID, model.MetricNameFirstResponse, time.Date(2026, 2, 11, 15, 0, 0, 0, time.UTC), nil, nil, nil) createReportingEvent(t, db, invalid.ID, model.MetricNameFirstResponse, time.Date(2026, 2, 11, 15, 0, 0, 0, time.UTC), nil, nil, nil) require.NoError(t, rollups.ComputeDailyRollup(context.Background(), blank.ID, time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC))) require.NoError(t, rollups.ComputeDailyRollup(context.Background(), invalid.ID, time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC))) var count int64 require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Count(&count).Error) assert.Equal(t, int64(0), count) } func TestReportingRollupUsesReportingTimezoneCreatedAtBoundaries(t *testing.T) { db, rollups, _ := setupReportingRollupTZTest(t) account := createReportingAccount(t, db, "America/New_York") user, inbox, conversation := createReportingActorRows(t, db, account.ID) createdAt := time.Date(2026, 2, 12, 4, 0, 0, 0, time.UTC) createReportingEvent(t, db, account.ID, "conversation_resolved", createdAt, &user.ID, &inbox.ID, &conversation.ID) require.NoError(t, rollups.ComputeDailyRollup(context.Background(), account.ID, time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC))) rollupDate := time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC) var resolutionTime model.ReportingEventsRollup require.NoError(t, db.Where("account_id = ? AND date = ? AND dimension_type = ? AND dimension_id = ? AND metric = ?", account.ID, rollupDate, model.DimensionAccount, account.ID, model.MetricResolutionTime).First(&resolutionTime).Error) assert.Equal(t, int64(1), resolutionTime.Count) assert.Equal(t, 100.0, resolutionTime.SumValue) var resolutionsCount model.ReportingEventsRollup require.NoError(t, db.Where("account_id = ? AND date = ? AND dimension_type = ? AND dimension_id = ? AND metric = ?", account.ID, rollupDate, model.DimensionAccount, account.ID, model.MetricResolutionsCount).First(&resolutionsCount).Error) assert.Equal(t, int64(1), resolutionsCount.Count) assert.Equal(t, 0.0, resolutionsCount.SumValue) } func TestAnalyticsRecordEventAppliesAdditiveLiveRollup(t *testing.T) { db, _, _ := setupReportingRollupTZTest(t) account := createReportingAccount(t, db, "America/New_York") user, inbox, conversation := createReportingActorRows(t, db, account.ID) rollupDate := time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC) require.NoError(t, db.Create(&model.ReportingEventsRollup{ AccountID: account.ID, Date: rollupDate, DimensionType: model.DimensionAccount, DimensionID: account.ID, Metric: model.MetricResolutionTime, Count: 7, SumValue: 700, SumValueBusinessHours: 350, }).Error) svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db)) createdAt := time.Date(2026, 2, 12, 4, 0, 0, 0, time.UTC) event := &model.ReportingEvent{ Base: model.Base{CreatedAt: createdAt}, AccountID: account.ID, Name: "conversation_resolved", Value: 100, ValueInBusinessHours: 50, UserID: &user.ID, InboxID: &inbox.ID, ConversationID: &conversation.ID, EventStartTime: createdAt.Add(-time.Hour), EventEndTime: createdAt, } require.NoError(t, svc.RecordEvent(context.Background(), event)) var rawCount int64 require.NoError(t, db.Model(&model.ReportingEvent{}).Where("account_id = ? AND name = ?", account.ID, "conversation_resolved").Count(&rawCount).Error) assert.Equal(t, int64(1), rawCount) var resolutionTime model.ReportingEventsRollup require.NoError(t, db.Where("account_id = ? AND date = ? AND dimension_type = ? AND dimension_id = ? AND metric = ?", account.ID, rollupDate, model.DimensionAccount, account.ID, model.MetricResolutionTime).First(&resolutionTime).Error) assert.Equal(t, int64(8), resolutionTime.Count) assert.Equal(t, 800.0, resolutionTime.SumValue) assert.Equal(t, 400.0, resolutionTime.SumValueBusinessHours) var resolutionsCount model.ReportingEventsRollup require.NoError(t, db.Where("account_id = ? AND date = ? AND dimension_type = ? AND dimension_id = ? AND metric = ?", account.ID, rollupDate, model.DimensionAccount, account.ID, model.MetricResolutionsCount).First(&resolutionsCount).Error) assert.Equal(t, int64(1), resolutionsCount.Count) assert.Equal(t, 0.0, resolutionsCount.SumValue) var agentRows int64 require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Where("account_id = ? AND date = ? AND dimension_type = ? AND dimension_id = ?", account.ID, rollupDate, model.DimensionAgent, user.ID).Count(&agentRows).Error) assert.Equal(t, int64(2), agentRows) } func TestReportingRollupIgnoresUnknownEventMetrics(t *testing.T) { db, rollups, _ := setupReportingRollupTZTest(t) account := createReportingAccount(t, db, "America/New_York") createReportingEvent(t, db, account.ID, "conversation_opened", time.Date(2026, 2, 11, 15, 0, 0, 0, time.UTC), nil, nil, nil) require.NoError(t, rollups.ComputeDailyRollup(context.Background(), account.ID, time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC))) var count int64 require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Where("account_id = ?", account.ID).Count(&count).Error) assert.Equal(t, int64(0), count) } func TestReportingRollupSkipsNilAgentAndInboxDimensions(t *testing.T) { db, rollups, _ := setupReportingRollupTZTest(t) account := createReportingAccount(t, db, "America/New_York") createReportingEvent(t, db, account.ID, model.MetricNameFirstResponse, time.Date(2026, 2, 11, 15, 0, 0, 0, time.UTC), nil, nil, nil) require.NoError(t, rollups.ComputeDailyRollup(context.Background(), account.ID, time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC))) var accountRows int64 require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Where("account_id = ? AND dimension_type = ?", account.ID, model.DimensionAccount).Count(&accountRows).Error) assert.Equal(t, int64(1), accountRows) var nilDimensionFallbacks int64 require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Where("account_id = ? AND dimension_type IN ?", account.ID, []model.DimensionType{model.DimensionAgent, model.DimensionInbox}).Count(&nilDimensionFallbacks).Error) assert.Equal(t, int64(0), nilDimensionFallbacks) } func TestReportingBackfillUsesReportingTimezoneAndDistinctBotHandoffs(t *testing.T) { db, _, backfill := setupReportingRollupTZTest(t) account := createReportingAccount(t, db, "America/New_York") user, inbox, conversationA := createReportingActorRows(t, db, account.ID) conversationB := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: conversationA.ContactID, AssigneeID: &user.ID, ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} require.NoError(t, db.Create(conversationB).Error) createReportingEvent(t, db, account.ID, "conversation_bot_handoff", time.Date(2026, 2, 11, 4, 59, 0, 0, time.UTC), &user.ID, &inbox.ID, &conversationA.ID) createReportingEvent(t, db, account.ID, "conversation_bot_handoff", time.Date(2026, 2, 11, 5, 0, 0, 0, time.UTC), &user.ID, &inbox.ID, &conversationA.ID) createReportingEvent(t, db, account.ID, "conversation_bot_handoff", time.Date(2026, 2, 11, 6, 0, 0, 0, time.UTC), &user.ID, &inbox.ID, &conversationA.ID) createReportingEvent(t, db, account.ID, "conversation_bot_handoff", time.Date(2026, 2, 12, 4, 59, 0, 0, time.UTC), &user.ID, &inbox.ID, &conversationB.ID) createReportingEvent(t, db, account.ID, "conversation_bot_handoff", time.Date(2026, 2, 12, 5, 0, 0, 0, time.UTC), &user.ID, &inbox.ID, &conversationB.ID) require.NoError(t, backfill.BackfillDate(context.Background(), account.ID, time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC))) rollupDate := time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC) for _, dimension := range []struct { typ model.DimensionType id uint }{ {model.DimensionAccount, account.ID}, {model.DimensionAgent, user.ID}, {model.DimensionInbox, inbox.ID}, } { var rollup model.ReportingEventsRollup require.NoError(t, db.Where("account_id = ? AND date = ? AND dimension_type = ? AND dimension_id = ? AND metric = ?", account.ID, rollupDate, dimension.typ, dimension.id, model.MetricBotHandoffsCount).First(&rollup).Error) assert.Equal(t, int64(2), rollup.Count) assert.Equal(t, 0.0, rollup.SumValue) } }