package usecase_test import ( "context" "testing" "time" "apps/backend/internal/module/usage/domain" "apps/backend/internal/module/usage/repository" "apps/backend/internal/module/usage/usecase" "github.com/stretchr/testify/require" ) func analyticsNano(t *testing.T, value string) int64 { t.Helper() parsed, err := time.Parse(time.RFC3339, value) require.NoError(t, err) return parsed.UnixNano() } func TestParseAnalyticsQueryValidationAndCaps(t *testing.T) { _, err := usecase.ParseAnalyticsQuery("week", "2026-01-01", "2026-01-02") require.ErrorIs(t, err, domain.ErrInvalidRange) _, err = usecase.ParseAnalyticsQuery("day", "2026-01-03", "2026-01-02") require.ErrorIs(t, err, domain.ErrInvalidRange) _, err = usecase.ParseAnalyticsQuery("day", "2026-01-01", "2026-04-04") require.ErrorIs(t, err, domain.ErrInvalidRange) _, err = usecase.ParseAnalyticsQuery("month", "2023-01-01", "2025-12-31") require.NoError(t, err) } func TestTenantAnalyticsUsesInclusiveUTCBoundsAndSeparateModes(t *testing.T) { repo := repository.NewMemory() for _, event := range []*domain.Event{ {ID: "before", UID: 1, Meter: domain.MeterAICopy, Credits: 9, KeyMode: domain.KeyModePlatform, CreatedAt: analyticsNano(t, "2026-01-31T23:59:59Z")}, {ID: "first", UID: 1, Meter: domain.MeterAICopy, Credits: 2, KeyMode: domain.KeyModePlatform, CreatedAt: analyticsNano(t, "2026-02-01T00:00:00Z")}, {ID: "byok", UID: 1, Meter: domain.MeterWebSearch, Credits: 99, KeyMode: domain.KeyModeByok, CreatedAt: analyticsNano(t, "2026-02-02T12:00:00Z")}, {ID: "last", UID: 1, Meter: domain.MeterWebSearch, Credits: 1, KeyMode: domain.KeyModePlatform, CreatedAt: analyticsNano(t, "2026-02-02T23:59:59.999999999Z")}, {ID: "after", UID: 1, Meter: domain.MeterAICopy, Credits: 9, KeyMode: domain.KeyModePlatform, CreatedAt: analyticsNano(t, "2026-02-03T00:00:00Z")}, } { require.NoError(t, repo.InsertEvent(context.Background(), event)) } query, err := usecase.ParseAnalyticsQuery("day", "2026-02-01", "2026-02-02") require.NoError(t, err) result, err := usecase.New(repo, nil).TenantAnalytics(context.Background(), query, []domain.AnalyticsMember{{UID: 1, Email: "one@example.test", DisplayName: "One"}}) require.NoError(t, err) require.Equal(t, 3, result.ConsumedCredits) require.Equal(t, 1, result.AICalls) require.Equal(t, 1, result.SearchCalls) require.Equal(t, 1, result.Byok.CallCount) require.Equal(t, 0, result.Byok.ByMeter[domain.MeterWebSearch].Credits) require.Equal(t, 1, result.Byok.ByMeter[domain.MeterWebSearch].Count) require.Len(t, result.Series, 2) require.Equal(t, 2, result.Series[0].ConsumedCredits) require.Equal(t, 1, result.Series[1].ConsumedCredits) require.Equal(t, 1, result.Series[1].ByokCallCount) require.Equal(t, 3, result.Members[0].TotalCredits) require.Equal(t, 1, result.Members[0].ByokCallCount) } func TestMemoryListEventsRangeUsesExclusiveUpperBound(t *testing.T) { repo := repository.NewMemory() for i, nano := range []int64{99, 100, 199, 200} { require.NoError(t, repo.InsertEvent(context.Background(), &domain.Event{ID: string(rune('a' + i)), CreatedAt: nano})) } list, err := repo.ListEventsRange(context.Background(), 100, 200) require.NoError(t, err) require.Len(t, list, 2) require.Equal(t, int64(100), list[0].CreatedAt) require.Equal(t, int64(199), list[1].CreatedAt) }