//go:build unit package server_test import ( "bytes" "context" "errors" "io" "math" "net/http" "net/http/httptest" "sort" "testing" "time" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/handler" adminhandler "github.com/Wei-Shaw/sub2api/internal/handler/admin" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) func TestAPIContracts(t *testing.T) { gin.SetMode(gin.TestMode) tests := []struct { name string setup func(t *testing.T, deps *contractDeps) method string path string body string headers map[string]string wantStatus int wantJSON string }{ { name: "GET /api/v1/auth/me", method: http.MethodGet, path: "/api/v1/auth/me", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "id": 1, "email": "alice@example.com", "username": "alice", "role": "user", "balance": 12.5, "concurrency": 5, "status": "active", "allowed_groups": null, "created_at": "2025-01-02T03:04:05Z", "updated_at": "2025-01-02T03:04:05Z", "run_mode": "standard" } }`, }, { name: "POST /api/v1/keys", method: http.MethodPost, path: "/api/v1/keys", body: `{"name":"Key One","custom_key":"sk_custom_1234567890"}`, headers: map[string]string{ "Content-Type": "application/json", }, wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "id": 100, "user_id": 1, "key": "sk_custom_1234567890", "name": "Key One", "group_id": null, "status": "active", "ip_whitelist": null, "ip_blacklist": null, "quota": 0, "quota_used": 0, "expires_at": null, "created_at": "2025-01-02T03:04:05Z", "updated_at": "2025-01-02T03:04:05Z" } }`, }, { name: "GET /api/v1/keys (paginated)", setup: func(t *testing.T, deps *contractDeps) { t.Helper() deps.apiKeyRepo.MustSeed(&service.APIKey{ ID: 100, UserID: 1, Key: "sk_custom_1234567890", Name: "Key One", Status: service.StatusActive, CreatedAt: deps.now, UpdatedAt: deps.now, }) }, method: http.MethodGet, path: "/api/v1/keys?page=1&page_size=10", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "items": [ { "id": 100, "user_id": 1, "key": "sk_custom_1234567890", "name": "Key One", "group_id": null, "status": "active", "ip_whitelist": null, "ip_blacklist": null, "quota": 0, "quota_used": 0, "expires_at": null, "created_at": "2025-01-02T03:04:05Z", "updated_at": "2025-01-02T03:04:05Z" } ], "total": 1, "page": 1, "page_size": 10, "pages": 1 } }`, }, { name: "GET /api/v1/groups/available", setup: func(t *testing.T, deps *contractDeps) { t.Helper() // 普通用户可见的分组列表不应包含内部字段(如 model_routing/account_count)。 deps.groupRepo.SetActive([]service.Group{ { ID: 10, Name: "Group One", Description: "desc", Platform: service.PlatformAnthropic, RateMultiplier: 1.5, IsExclusive: false, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, ModelRoutingEnabled: true, ModelRouting: map[string][]int64{ "claude-3-*": []int64{101, 102}, }, AccountCount: 2, CreatedAt: deps.now, UpdatedAt: deps.now, }, }) deps.userSubRepo.SetActiveByUserID(1, nil) }, method: http.MethodGet, path: "/api/v1/groups/available", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": [ { "id": 10, "name": "Group One", "description": "desc", "platform": "anthropic", "rate_multiplier": 1.5, "is_exclusive": false, "status": "active", "subscription_type": "standard", "daily_limit_usd": null, "weekly_limit_usd": null, "monthly_limit_usd": null, "image_price_1k": null, "image_price_2k": null, "image_price_4k": null, "claude_code_only": false, "fallback_group_id": null, "fallback_group_id_on_invalid_request": null, "created_at": "2025-01-02T03:04:05Z", "updated_at": "2025-01-02T03:04:05Z" } ] }`, }, { name: "GET /api/v1/subscriptions", setup: func(t *testing.T, deps *contractDeps) { t.Helper() // 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。 deps.userSubRepo.SetByUserID(1, []service.UserSubscription{ { ID: 501, UserID: 1, GroupID: 10, StartsAt: deps.now, ExpiresAt: time.Date(2099, 1, 2, 3, 4, 5, 0, time.UTC), // 使用未来日期避免 normalizeSubscriptionStatus 标记为过期 Status: service.SubscriptionStatusActive, DailyUsageUSD: 1.23, WeeklyUsageUSD: 2.34, MonthlyUsageUSD: 3.45, AssignedBy: ptr(int64(999)), AssignedAt: deps.now, Notes: "admin-note", CreatedAt: deps.now, UpdatedAt: deps.now, }, }) }, method: http.MethodGet, path: "/api/v1/subscriptions", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": [ { "id": 501, "user_id": 1, "group_id": 10, "starts_at": "2025-01-02T03:04:05Z", "expires_at": "2099-01-02T03:04:05Z", "status": "active", "daily_window_start": null, "weekly_window_start": null, "monthly_window_start": null, "daily_usage_usd": 1.23, "weekly_usage_usd": 2.34, "monthly_usage_usd": 3.45, "created_at": "2025-01-02T03:04:05Z", "updated_at": "2025-01-02T03:04:05Z" } ] }`, }, { name: "GET /api/v1/redeem/history", setup: func(t *testing.T, deps *contractDeps) { t.Helper() // 普通用户兑换历史不应包含 notes 等内部字段。 deps.redeemRepo.SetByUser(1, []service.RedeemCode{ { ID: 900, Code: "CODE-123", Type: service.RedeemTypeBalance, Value: 1.25, Status: service.StatusUsed, UsedBy: ptr(int64(1)), UsedAt: ptr(deps.now), Notes: "internal-note", CreatedAt: deps.now, }, }) }, method: http.MethodGet, path: "/api/v1/redeem/history", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": [ { "id": 900, "code": "CODE-123", "type": "balance", "value": 1.25, "status": "used", "used_by": 1, "used_at": "2025-01-02T03:04:05Z", "created_at": "2025-01-02T03:04:05Z", "group_id": null, "validity_days": 0 } ] }`, }, { name: "GET /api/v1/usage/stats", setup: func(t *testing.T, deps *contractDeps) { t.Helper() deps.usageRepo.SetUserLogs(1, []service.UsageLog{ { ID: 1, UserID: 1, APIKeyID: 100, AccountID: 200, Model: "claude-3", InputTokens: 10, OutputTokens: 20, CacheCreationTokens: 1, CacheReadTokens: 2, TotalCost: 0.5, ActualCost: 0.5, DurationMs: ptr(100), CreatedAt: deps.now, }, { ID: 2, UserID: 1, APIKeyID: 100, AccountID: 200, Model: "claude-3", InputTokens: 5, OutputTokens: 15, TotalCost: 0.25, ActualCost: 0.25, DurationMs: ptr(300), CreatedAt: deps.now, }, }) }, method: http.MethodGet, path: "/api/v1/usage/stats?start_date=2025-01-01&end_date=2025-01-02", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "total_requests": 2, "total_input_tokens": 15, "total_output_tokens": 35, "total_cache_tokens": 3, "total_tokens": 53, "total_cost": 0.75, "total_actual_cost": 0.75, "average_duration_ms": 200 } }`, }, { name: "GET /api/v1/usage (paginated)", setup: func(t *testing.T, deps *contractDeps) { t.Helper() deps.usageRepo.SetUserLogs(1, []service.UsageLog{ { ID: 1, UserID: 1, APIKeyID: 100, AccountID: 200, AccountRateMultiplier: ptr(0.5), RequestID: "req_123", Model: "claude-3", InputTokens: 10, OutputTokens: 20, CacheCreationTokens: 1, CacheReadTokens: 2, TotalCost: 0.5, ActualCost: 0.5, RateMultiplier: 1, BillingType: service.BillingTypeBalance, Stream: true, DurationMs: ptr(100), FirstTokenMs: ptr(50), CreatedAt: deps.now, }, }) }, method: http.MethodGet, path: "/api/v1/usage?page=1&page_size=10", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "items": [ { "id": 1, "user_id": 1, "api_key_id": 100, "account_id": 200, "request_id": "req_123", "model": "claude-3", "group_id": null, "subscription_id": null, "input_tokens": 10, "output_tokens": 20, "cache_creation_tokens": 1, "cache_read_tokens": 2, "cache_creation_5m_tokens": 0, "cache_creation_1h_tokens": 0, "input_cost": 0, "output_cost": 0, "cache_creation_cost": 0, "cache_read_cost": 0, "total_cost": 0.5, "actual_cost": 0.5, "rate_multiplier": 1, "billing_type": 0, "stream": true, "duration_ms": 100, "first_token_ms": 50, "image_count": 0, "image_size": null, "created_at": "2025-01-02T03:04:05Z", "user_agent": null } ], "total": 1, "page": 1, "page_size": 10, "pages": 1 } }`, }, { name: "GET /api/v1/admin/settings", setup: func(t *testing.T, deps *contractDeps) { t.Helper() deps.settingRepo.SetAll(map[string]string{ service.SettingKeyRegistrationEnabled: "true", service.SettingKeyEmailVerifyEnabled: "false", service.SettingKeyPromoCodeEnabled: "true", service.SettingKeySMTPHost: "smtp.example.com", service.SettingKeySMTPPort: "587", service.SettingKeySMTPUsername: "user", service.SettingKeySMTPPassword: "secret", service.SettingKeySMTPFrom: "no-reply@example.com", service.SettingKeySMTPFromName: "Sub2API", service.SettingKeySMTPUseTLS: "true", service.SettingKeyTurnstileEnabled: "true", service.SettingKeyTurnstileSiteKey: "site-key", service.SettingKeyTurnstileSecretKey: "secret-key", service.SettingKeySiteName: "Sub2API", service.SettingKeySiteLogo: "", service.SettingKeySiteSubtitle: "Subtitle", service.SettingKeyAPIBaseURL: "https://api.example.com", service.SettingKeyContactInfo: "support", service.SettingKeyDocURL: "https://docs.example.com", service.SettingKeyDefaultConcurrency: "5", service.SettingKeyDefaultBalance: "1.25", service.SettingKeyOpsMonitoringEnabled: "false", service.SettingKeyOpsRealtimeMonitoringEnabled: "true", service.SettingKeyOpsQueryModeDefault: "auto", service.SettingKeyOpsMetricsIntervalSeconds: "60", }) }, method: http.MethodGet, path: "/api/v1/admin/settings", wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "registration_enabled": true, "email_verify_enabled": false, "promo_code_enabled": true, "password_reset_enabled": false, "totp_enabled": false, "totp_encryption_key_configured": false, "smtp_host": "smtp.example.com", "smtp_port": 587, "smtp_username": "user", "smtp_password_configured": true, "smtp_from_email": "no-reply@example.com", "smtp_from_name": "Sub2API", "smtp_use_tls": true, "turnstile_enabled": true, "turnstile_site_key": "site-key", "turnstile_secret_key_configured": true, "linuxdo_connect_enabled": false, "linuxdo_connect_client_id": "", "linuxdo_connect_client_secret_configured": false, "linuxdo_connect_redirect_url": "", "ops_monitoring_enabled": false, "ops_realtime_monitoring_enabled": true, "ops_query_mode_default": "auto", "ops_metrics_interval_seconds": 60, "site_name": "Sub2API", "site_logo": "", "site_subtitle": "Subtitle", "api_base_url": "https://api.example.com", "contact_info": "support", "doc_url": "https://docs.example.com", "default_concurrency": 5, "default_balance": 1.25, "enable_model_fallback": false, "fallback_model_anthropic": "claude-3-5-sonnet-20241022", "fallback_model_antigravity": "gemini-2.5-pro", "fallback_model_gemini": "gemini-2.5-pro", "fallback_model_openai": "gpt-4o", "enable_identity_patch": true, "identity_patch_prompt": "", "invitation_code_enabled": false, "home_content": "", "hide_ccs_import_button": false, "purchase_subscription_enabled": false, "purchase_subscription_url": "" } }`, }, { name: "POST /api/v1/admin/accounts/bulk-update", method: http.MethodPost, path: "/api/v1/admin/accounts/bulk-update", body: `{"account_ids":[101,102],"schedulable":false}`, headers: map[string]string{ "Content-Type": "application/json", }, wantStatus: http.StatusOK, wantJSON: `{ "code": 0, "message": "success", "data": { "success": 2, "failed": 0, "success_ids": [101, 102], "failed_ids": [], "results": [ {"account_id": 101, "success": true}, {"account_id": 102, "success": true} ] } }`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { deps := newContractDeps(t) if tt.setup != nil { tt.setup(t, deps) } status, body := doRequest(t, deps.router, tt.method, tt.path, tt.body, tt.headers) require.Equal(t, tt.wantStatus, status) require.JSONEq(t, tt.wantJSON, body) }) } } type contractDeps struct { now time.Time router http.Handler apiKeyRepo *stubApiKeyRepo groupRepo *stubGroupRepo userSubRepo *stubUserSubscriptionRepo usageRepo *stubUsageLogRepo settingRepo *stubSettingRepo redeemRepo *stubRedeemCodeRepo } func newContractDeps(t *testing.T) *contractDeps { t.Helper() now := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC) userRepo := &stubUserRepo{ users: map[int64]*service.User{ 1: { ID: 1, Email: "alice@example.com", Username: "alice", Notes: "hello", Role: service.RoleUser, Balance: 12.5, Concurrency: 5, Status: service.StatusActive, AllowedGroups: nil, CreatedAt: now, UpdatedAt: now, }, }, } apiKeyRepo := newStubApiKeyRepo(now) apiKeyCache := stubApiKeyCache{} groupRepo := &stubGroupRepo{} userSubRepo := &stubUserSubscriptionRepo{} accountRepo := stubAccountRepo{} proxyRepo := stubProxyRepo{} redeemRepo := &stubRedeemCodeRepo{} cfg := &config.Config{ Default: config.DefaultConfig{ APIKeyPrefix: "sk-", }, RunMode: config.RunModeStandard, } userService := service.NewUserService(userRepo, nil) apiKeyService := service.NewAPIKeyService(apiKeyRepo, userRepo, groupRepo, userSubRepo, nil, apiKeyCache, cfg) usageRepo := newStubUsageLogRepo() usageService := service.NewUsageService(usageRepo, userRepo, nil, nil) subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil) subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService) redeemService := service.NewRedeemService(redeemRepo, userRepo, subscriptionService, nil, nil, nil, nil) redeemHandler := handler.NewRedeemHandler(redeemService) settingRepo := newStubSettingRepo() settingService := service.NewSettingService(settingRepo, cfg) adminService := service.NewAdminService(userRepo, groupRepo, &accountRepo, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil) authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil) apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService) usageHandler := handler.NewUsageHandler(usageService, apiKeyService) adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil, nil, nil) adminAccountHandler := adminhandler.NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) jwtAuth := func(c *gin.Context) { c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{ UserID: 1, Concurrency: 5, }) c.Set(string(middleware.ContextKeyUserRole), service.RoleUser) c.Next() } adminAuth := func(c *gin.Context) { c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{ UserID: 1, Concurrency: 5, }) c.Set(string(middleware.ContextKeyUserRole), service.RoleAdmin) c.Next() } r := gin.New() v1 := r.Group("/api/v1") v1Auth := v1.Group("") v1Auth.Use(jwtAuth) v1Auth.GET("/auth/me", authHandler.GetCurrentUser) v1Keys := v1.Group("") v1Keys.Use(jwtAuth) v1Keys.GET("/keys", apiKeyHandler.List) v1Keys.POST("/keys", apiKeyHandler.Create) v1Keys.GET("/groups/available", apiKeyHandler.GetAvailableGroups) v1Usage := v1.Group("") v1Usage.Use(jwtAuth) v1Usage.GET("/usage", usageHandler.List) v1Usage.GET("/usage/stats", usageHandler.Stats) v1Subs := v1.Group("") v1Subs.Use(jwtAuth) v1Subs.GET("/subscriptions", subscriptionHandler.List) v1Redeem := v1.Group("") v1Redeem.Use(jwtAuth) v1Redeem.GET("/redeem/history", redeemHandler.GetHistory) v1Admin := v1.Group("/admin") v1Admin.Use(adminAuth) v1Admin.GET("/settings", adminSettingHandler.GetSettings) v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate) return &contractDeps{ now: now, router: r, apiKeyRepo: apiKeyRepo, groupRepo: groupRepo, userSubRepo: userSubRepo, usageRepo: usageRepo, settingRepo: settingRepo, redeemRepo: redeemRepo, } } func doRequest(t *testing.T, router http.Handler, method, path, body string, headers map[string]string) (int, string) { t.Helper() req := httptest.NewRequest(method, path, bytes.NewBufferString(body)) for k, v := range headers { req.Header.Set(k, v) } w := httptest.NewRecorder() router.ServeHTTP(w, req) respBody, err := io.ReadAll(w.Result().Body) require.NoError(t, err) return w.Result().StatusCode, string(respBody) } func ptr[T any](v T) *T { return &v } type stubUserRepo struct { users map[int64]*service.User } func (r *stubUserRepo) Create(ctx context.Context, user *service.User) error { return errors.New("not implemented") } func (r *stubUserRepo) GetByID(ctx context.Context, id int64) (*service.User, error) { user, ok := r.users[id] if !ok { return nil, service.ErrUserNotFound } clone := *user return &clone, nil } func (r *stubUserRepo) GetByEmail(ctx context.Context, email string) (*service.User, error) { for _, user := range r.users { if user.Email == email { clone := *user return &clone, nil } } return nil, service.ErrUserNotFound } func (r *stubUserRepo) GetFirstAdmin(ctx context.Context) (*service.User, error) { for _, user := range r.users { if user.Role == service.RoleAdmin && user.Status == service.StatusActive { clone := *user return &clone, nil } } return nil, service.ErrUserNotFound } func (r *stubUserRepo) Update(ctx context.Context, user *service.User) error { return errors.New("not implemented") } func (r *stubUserRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (r *stubUserRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUserRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUserRepo) UpdateBalance(ctx context.Context, id int64, amount float64) error { return errors.New("not implemented") } func (r *stubUserRepo) DeductBalance(ctx context.Context, id int64, amount float64) error { return errors.New("not implemented") } func (r *stubUserRepo) UpdateConcurrency(ctx context.Context, id int64, amount int) error { return errors.New("not implemented") } func (r *stubUserRepo) ExistsByEmail(ctx context.Context, email string) (bool, error) { return false, errors.New("not implemented") } func (r *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) { return 0, errors.New("not implemented") } func (r *stubUserRepo) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error { return errors.New("not implemented") } func (r *stubUserRepo) EnableTotp(ctx context.Context, userID int64) error { return errors.New("not implemented") } func (r *stubUserRepo) DisableTotp(ctx context.Context, userID int64) error { return errors.New("not implemented") } type stubApiKeyCache struct{} func (stubApiKeyCache) GetCreateAttemptCount(ctx context.Context, userID int64) (int, error) { return 0, nil } func (stubApiKeyCache) IncrementCreateAttemptCount(ctx context.Context, userID int64) error { return nil } func (stubApiKeyCache) DeleteCreateAttemptCount(ctx context.Context, userID int64) error { return nil } func (stubApiKeyCache) IncrementDailyUsage(ctx context.Context, apiKey string) error { return nil } func (stubApiKeyCache) SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error { return nil } func (stubApiKeyCache) GetAuthCache(ctx context.Context, key string) (*service.APIKeyAuthCacheEntry, error) { return nil, nil } func (stubApiKeyCache) SetAuthCache(ctx context.Context, key string, entry *service.APIKeyAuthCacheEntry, ttl time.Duration) error { return nil } func (stubApiKeyCache) DeleteAuthCache(ctx context.Context, key string) error { return nil } func (stubApiKeyCache) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error { return nil } func (stubApiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error { return nil } type stubGroupRepo struct { active []service.Group } func (r *stubGroupRepo) SetActive(groups []service.Group) { r.active = append([]service.Group(nil), groups...) } func (stubGroupRepo) Create(ctx context.Context, group *service.Group) error { return errors.New("not implemented") } func (stubGroupRepo) GetByID(ctx context.Context, id int64) (*service.Group, error) { return nil, service.ErrGroupNotFound } func (stubGroupRepo) GetByIDLite(ctx context.Context, id int64) (*service.Group, error) { return nil, service.ErrGroupNotFound } func (stubGroupRepo) Update(ctx context.Context, group *service.Group) error { return errors.New("not implemented") } func (stubGroupRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (stubGroupRepo) DeleteCascade(ctx context.Context, id int64) ([]int64, error) { return nil, errors.New("not implemented") } func (stubGroupRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Group, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubGroupRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]service.Group, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubGroupRepo) ListActive(ctx context.Context) ([]service.Group, error) { return append([]service.Group(nil), r.active...), nil } func (r *stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) { out := make([]service.Group, 0, len(r.active)) for i := range r.active { g := r.active[i] if g.Platform == platform { out = append(out, g) } } return out, nil } func (stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error) { return false, errors.New("not implemented") } func (stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, error) { return 0, errors.New("not implemented") } func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) { return 0, errors.New("not implemented") } func (stubGroupRepo) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error { return errors.New("not implemented") } func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) { return nil, errors.New("not implemented") } func (stubGroupRepo) UpdateSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error { return nil } type stubAccountRepo struct { bulkUpdateIDs []int64 } func (s *stubAccountRepo) Create(ctx context.Context, account *service.Account) error { return errors.New("not implemented") } func (s *stubAccountRepo) GetByID(ctx context.Context, id int64) (*service.Account, error) { return nil, service.ErrAccountNotFound } func (s *stubAccountRepo) GetByIDs(ctx context.Context, ids []int64) ([]*service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ExistsByID(ctx context.Context, id int64) (bool, error) { return false, errors.New("not implemented") } func (s *stubAccountRepo) GetByCRSAccountID(ctx context.Context, crsAccountID string) (*service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) Update(ctx context.Context, account *service.Account) error { return errors.New("not implemented") } func (s *stubAccountRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Account, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (s *stubAccountRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string) ([]service.Account, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (s *stubAccountRepo) ListByGroup(ctx context.Context, groupID int64) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListActive(ctx context.Context) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListByPlatform(ctx context.Context, platform string) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) UpdateLastUsed(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error { return errors.New("not implemented") } func (s *stubAccountRepo) SetError(ctx context.Context, id int64, errorMsg string) error { return errors.New("not implemented") } func (s *stubAccountRepo) ClearError(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) SetSchedulable(ctx context.Context, id int64, schedulable bool) error { return errors.New("not implemented") } func (s *stubAccountRepo) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) { return 0, errors.New("not implemented") } func (s *stubAccountRepo) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) ListSchedulable(ctx context.Context) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListSchedulableByPlatform(ctx context.Context, platform string) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatform(ctx context.Context, groupID int64, platform string) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListSchedulableByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatforms(ctx context.Context, groupID int64, platforms []string) ([]service.Account, error) { return nil, errors.New("not implemented") } func (s *stubAccountRepo) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error { return errors.New("not implemented") } func (s *stubAccountRepo) SetAntigravityQuotaScopeLimit(ctx context.Context, id int64, scope service.AntigravityQuotaScope, resetAt time.Time) error { return errors.New("not implemented") } func (s *stubAccountRepo) SetModelRateLimit(ctx context.Context, id int64, scope string, resetAt time.Time) error { return errors.New("not implemented") } func (s *stubAccountRepo) SetOverloaded(ctx context.Context, id int64, until time.Time) error { return errors.New("not implemented") } func (s *stubAccountRepo) SetTempUnschedulable(ctx context.Context, id int64, until time.Time, reason string) error { return errors.New("not implemented") } func (s *stubAccountRepo) ClearTempUnschedulable(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) ClearRateLimit(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) ClearAntigravityQuotaScopes(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) ClearModelRateLimits(ctx context.Context, id int64) error { return errors.New("not implemented") } func (s *stubAccountRepo) UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error { return errors.New("not implemented") } func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error { return errors.New("not implemented") } func (s *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) { s.bulkUpdateIDs = append([]int64{}, ids...) return int64(len(ids)), nil } type stubProxyRepo struct{} func (stubProxyRepo) Create(ctx context.Context, proxy *service.Proxy) error { return errors.New("not implemented") } func (stubProxyRepo) GetByID(ctx context.Context, id int64) (*service.Proxy, error) { return nil, service.ErrProxyNotFound } func (stubProxyRepo) ListByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) { return nil, errors.New("not implemented") } func (stubProxyRepo) Update(ctx context.Context, proxy *service.Proxy) error { return errors.New("not implemented") } func (stubProxyRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (stubProxyRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Proxy, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubProxyRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.Proxy, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubProxyRepo) ListWithFiltersAndAccountCount(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.ProxyWithAccountCount, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubProxyRepo) ListActive(ctx context.Context) ([]service.Proxy, error) { return nil, errors.New("not implemented") } func (stubProxyRepo) ListActiveWithAccountCount(ctx context.Context) ([]service.ProxyWithAccountCount, error) { return nil, errors.New("not implemented") } func (stubProxyRepo) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) { return false, errors.New("not implemented") } func (stubProxyRepo) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) { return 0, errors.New("not implemented") } func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]service.ProxyAccountSummary, error) { return nil, errors.New("not implemented") } type stubRedeemCodeRepo struct { byUser map[int64][]service.RedeemCode } func (r *stubRedeemCodeRepo) SetByUser(userID int64, codes []service.RedeemCode) { if r.byUser == nil { r.byUser = make(map[int64][]service.RedeemCode) } r.byUser[userID] = append([]service.RedeemCode(nil), codes...) } func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error { return errors.New("not implemented") } func (stubRedeemCodeRepo) CreateBatch(ctx context.Context, codes []service.RedeemCode) error { return errors.New("not implemented") } func (stubRedeemCodeRepo) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) { return nil, service.ErrRedeemCodeNotFound } func (stubRedeemCodeRepo) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) { return nil, service.ErrRedeemCodeNotFound } func (stubRedeemCodeRepo) Update(ctx context.Context, code *service.RedeemCode) error { return errors.New("not implemented") } func (stubRedeemCodeRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (stubRedeemCodeRepo) Use(ctx context.Context, id, userID int64) error { return errors.New("not implemented") } func (stubRedeemCodeRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.RedeemCode, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) { if r.byUser == nil { return nil, nil } codes := r.byUser[userID] if limit > 0 && len(codes) > limit { codes = codes[:limit] } return append([]service.RedeemCode(nil), codes...), nil } func (stubRedeemCodeRepo) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]service.RedeemCode, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubRedeemCodeRepo) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) { return 0, errors.New("not implemented") } type stubUserSubscriptionRepo struct { byUser map[int64][]service.UserSubscription activeByUser map[int64][]service.UserSubscription } func (r *stubUserSubscriptionRepo) SetByUserID(userID int64, subs []service.UserSubscription) { if r.byUser == nil { r.byUser = make(map[int64][]service.UserSubscription) } r.byUser[userID] = append([]service.UserSubscription(nil), subs...) } func (r *stubUserSubscriptionRepo) SetActiveByUserID(userID int64, subs []service.UserSubscription) { if r.activeByUser == nil { r.activeByUser = make(map[int64][]service.UserSubscription) } r.activeByUser[userID] = append([]service.UserSubscription(nil), subs...) } func (stubUserSubscriptionRepo) Create(ctx context.Context, sub *service.UserSubscription) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) { return nil, errors.New("not implemented") } func (stubUserSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) { return nil, errors.New("not implemented") } func (stubUserSubscriptionRepo) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) { return nil, errors.New("not implemented") } func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSubscription) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) { if r.byUser == nil { return nil, nil } return append([]service.UserSubscription(nil), r.byUser[userID]...), nil } func (r *stubUserSubscriptionRepo) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) { if r.activeByUser == nil { return nil, nil } return append([]service.UserSubscription(nil), r.activeByUser[userID]...), nil } func (stubUserSubscriptionRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.UserSubscription, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubUserSubscriptionRepo) List(ctx context.Context, params pagination.PaginationParams, userID, groupID *int64, status, sortBy, sortOrder string) ([]service.UserSubscription, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (stubUserSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) { return false, errors.New("not implemented") } func (stubUserSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) UpdateStatus(ctx context.Context, subscriptionID int64, status string) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) UpdateNotes(ctx context.Context, subscriptionID int64, notes string) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) ActivateWindows(ctx context.Context, id int64, start time.Time) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) ResetDailyUsage(ctx context.Context, id int64, newWindowStart time.Time) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) ResetWeeklyUsage(ctx context.Context, id int64, newWindowStart time.Time) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) ResetMonthlyUsage(ctx context.Context, id int64, newWindowStart time.Time) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) IncrementUsage(ctx context.Context, id int64, costUSD float64) error { return errors.New("not implemented") } func (stubUserSubscriptionRepo) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) { return 0, errors.New("not implemented") } type stubApiKeyRepo struct { now time.Time nextID int64 byID map[int64]*service.APIKey byKey map[string]*service.APIKey } func newStubApiKeyRepo(now time.Time) *stubApiKeyRepo { return &stubApiKeyRepo{ now: now, nextID: 100, byID: make(map[int64]*service.APIKey), byKey: make(map[string]*service.APIKey), } } func (r *stubApiKeyRepo) MustSeed(key *service.APIKey) { if key == nil { return } clone := *key r.byID[clone.ID] = &clone r.byKey[clone.Key] = &clone } func (r *stubApiKeyRepo) Create(ctx context.Context, key *service.APIKey) error { if key == nil { return errors.New("nil key") } if key.ID == 0 { key.ID = r.nextID r.nextID++ } if key.CreatedAt.IsZero() { key.CreatedAt = r.now } if key.UpdatedAt.IsZero() { key.UpdatedAt = r.now } clone := *key r.byID[clone.ID] = &clone r.byKey[clone.Key] = &clone return nil } func (r *stubApiKeyRepo) GetByID(ctx context.Context, id int64) (*service.APIKey, error) { key, ok := r.byID[id] if !ok { return nil, service.ErrAPIKeyNotFound } clone := *key return &clone, nil } func (r *stubApiKeyRepo) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) { key, ok := r.byID[id] if !ok { return "", 0, service.ErrAPIKeyNotFound } return key.Key, key.UserID, nil } func (r *stubApiKeyRepo) GetByKey(ctx context.Context, key string) (*service.APIKey, error) { found, ok := r.byKey[key] if !ok { return nil, service.ErrAPIKeyNotFound } clone := *found return &clone, nil } func (r *stubApiKeyRepo) GetByKeyForAuth(ctx context.Context, key string) (*service.APIKey, error) { return r.GetByKey(ctx, key) } func (r *stubApiKeyRepo) Update(ctx context.Context, key *service.APIKey) error { if key == nil { return errors.New("nil key") } if _, ok := r.byID[key.ID]; !ok { return service.ErrAPIKeyNotFound } if key.UpdatedAt.IsZero() { key.UpdatedAt = r.now } clone := *key r.byID[clone.ID] = &clone r.byKey[clone.Key] = &clone return nil } func (r *stubApiKeyRepo) Delete(ctx context.Context, id int64) error { key, ok := r.byID[id] if !ok { return service.ErrAPIKeyNotFound } delete(r.byID, id) delete(r.byKey, key.Key) return nil } func (r *stubApiKeyRepo) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) { ids := make([]int64, 0, len(r.byID)) for id := range r.byID { if r.byID[id].UserID == userID { ids = append(ids, id) } } sort.Slice(ids, func(i, j int) bool { return ids[i] > ids[j] }) start := params.Offset() if start > len(ids) { start = len(ids) } end := start + params.Limit() if end > len(ids) { end = len(ids) } out := make([]service.APIKey, 0, end-start) for _, id := range ids[start:end] { clone := *r.byID[id] out = append(out, clone) } total := int64(len(ids)) pageSize := params.Limit() pages := int(math.Ceil(float64(total) / float64(pageSize))) if pages < 1 { pages = 1 } return out, &pagination.PaginationResult{ Total: total, Page: params.Page, PageSize: pageSize, Pages: pages, }, nil } func (r *stubApiKeyRepo) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) { if len(apiKeyIDs) == 0 { return []int64{}, nil } seen := make(map[int64]struct{}, len(apiKeyIDs)) out := make([]int64, 0, len(apiKeyIDs)) for _, id := range apiKeyIDs { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} key, ok := r.byID[id] if ok && key.UserID == userID { out = append(out, id) } } return out, nil } func (r *stubApiKeyRepo) CountByUserID(ctx context.Context, userID int64) (int64, error) { var count int64 for _, key := range r.byID { if key.UserID == userID { count++ } } return count, nil } func (r *stubApiKeyRepo) ExistsByKey(ctx context.Context, key string) (bool, error) { _, ok := r.byKey[key] return ok, nil } func (r *stubApiKeyRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubApiKeyRepo) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) { return nil, errors.New("not implemented") } func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) { return 0, errors.New("not implemented") } func (r *stubApiKeyRepo) CountByGroupID(ctx context.Context, groupID int64) (int64, error) { return 0, errors.New("not implemented") } func (r *stubApiKeyRepo) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) { return nil, errors.New("not implemented") } func (r *stubApiKeyRepo) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) { return nil, errors.New("not implemented") } func (r *stubApiKeyRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) { return 0, errors.New("not implemented") } type stubUsageLogRepo struct { userLogs map[int64][]service.UsageLog } func newStubUsageLogRepo() *stubUsageLogRepo { return &stubUsageLogRepo{userLogs: make(map[int64][]service.UsageLog)} } func (r *stubUsageLogRepo) SetUserLogs(userID int64, logs []service.UsageLog) { r.userLogs[userID] = logs } func (r *stubUsageLogRepo) Create(ctx context.Context, log *service.UsageLog) (bool, error) { return false, errors.New("not implemented") } func (r *stubUsageLogRepo) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) Delete(ctx context.Context, id int64) error { return errors.New("not implemented") } func (r *stubUsageLogRepo) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) { logs := r.userLogs[userID] total := int64(len(logs)) out := paginateLogs(logs, params) return out, paginationResult(total, params), nil } func (r *stubUsageLogRepo) ListByAPIKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUsageLogRepo) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUsageLogRepo) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { logs := r.userLogs[userID] return logs, paginationResult(int64(len(logs)), pagination.PaginationParams{Page: 1, PageSize: 100}), nil } func (r *stubUsageLogRepo) ListByAPIKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUsageLogRepo) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUsageLogRepo) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) { return nil, nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID, accountID, groupID int64, model string, stream *bool, billingType *int8) ([]usagestats.TrendDataPoint, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, stream *bool, billingType *int8) ([]usagestats.ModelStat, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { logs := r.userLogs[userID] if len(logs) == 0 { return &usagestats.UsageStats{}, nil } var totalRequests int64 var totalInputTokens int64 var totalOutputTokens int64 var totalCacheTokens int64 var totalCost float64 var totalActualCost float64 var totalDuration int64 var durationCount int64 for _, log := range logs { totalRequests++ totalInputTokens += int64(log.InputTokens) totalOutputTokens += int64(log.OutputTokens) totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens) totalCost += log.TotalCost totalActualCost += log.ActualCost if log.DurationMs != nil { totalDuration += int64(*log.DurationMs) durationCount++ } } var avgDuration float64 if durationCount > 0 { avgDuration = float64(totalDuration) / float64(durationCount) } return &usagestats.UsageStats{ TotalRequests: totalRequests, TotalInputTokens: totalInputTokens, TotalOutputTokens: totalOutputTokens, TotalCacheTokens: totalCacheTokens, TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens, TotalCost: totalCost, TotalActualCost: totalActualCost, AverageDurationMs: avgDuration, }, nil } func (r *stubUsageLogRepo) GetAPIKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetAccountStatsAggregated(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetModelStatsAggregated(ctx context.Context, modelName string, startTime, endTime time.Time) (*usagestats.UsageStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetDailyStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) ([]map[string]any, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*usagestats.BatchUserUsageStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetBatchAPIKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*usagestats.BatchAPIKeyUsageStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) { logs := r.userLogs[filters.UserID] // Apply filters var filtered []service.UsageLog for _, log := range logs { // Apply APIKeyID filter if filters.APIKeyID > 0 && log.APIKeyID != filters.APIKeyID { continue } // Apply Model filter if filters.Model != "" && log.Model != filters.Model { continue } // Apply Stream filter if filters.Stream != nil && log.Stream != *filters.Stream { continue } // Apply BillingType filter if filters.BillingType != nil && log.BillingType != *filters.BillingType { continue } // Apply time range filters if filters.StartTime != nil && log.CreatedAt.Before(*filters.StartTime) { continue } if filters.EndTime != nil && log.CreatedAt.After(*filters.EndTime) { continue } filtered = append(filtered, log) } total := int64(len(filtered)) out := paginateLogs(filtered, params) return out, paginationResult(total, params), nil } func (r *stubUsageLogRepo) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) { return nil, errors.New("not implemented") } func (r *stubUsageLogRepo) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) { return nil, errors.New("not implemented") } type stubSettingRepo struct { all map[string]string } func newStubSettingRepo() *stubSettingRepo { return &stubSettingRepo{all: make(map[string]string)} } func (r *stubSettingRepo) SetAll(values map[string]string) { r.all = make(map[string]string, len(values)) for k, v := range values { r.all[k] = v } } func (r *stubSettingRepo) Get(ctx context.Context, key string) (*service.Setting, error) { value, ok := r.all[key] if !ok { return nil, service.ErrSettingNotFound } return &service.Setting{Key: key, Value: value}, nil } func (r *stubSettingRepo) GetValue(ctx context.Context, key string) (string, error) { value, ok := r.all[key] if !ok { return "", service.ErrSettingNotFound } return value, nil } func (r *stubSettingRepo) Set(ctx context.Context, key, value string) error { r.all[key] = value return nil } func (r *stubSettingRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) { out := make(map[string]string, len(keys)) for _, key := range keys { out[key] = r.all[key] } return out, nil } func (r *stubSettingRepo) SetMultiple(ctx context.Context, settings map[string]string) error { for k, v := range settings { r.all[k] = v } return nil } func (r *stubSettingRepo) GetAll(ctx context.Context) (map[string]string, error) { out := make(map[string]string, len(r.all)) for k, v := range r.all { out[k] = v } return out, nil } func (r *stubSettingRepo) Delete(ctx context.Context, key string) error { delete(r.all, key) return nil } func paginateLogs(logs []service.UsageLog, params pagination.PaginationParams) []service.UsageLog { start := params.Offset() if start > len(logs) { start = len(logs) } end := start + params.Limit() if end > len(logs) { end = len(logs) } out := make([]service.UsageLog, 0, end-start) out = append(out, logs[start:end]...) return out } func paginationResult(total int64, params pagination.PaginationParams) *pagination.PaginationResult { pageSize := params.Limit() pages := int(math.Ceil(float64(total) / float64(pageSize))) if pages < 1 { pages = 1 } return &pagination.PaginationResult{ Total: total, Page: params.Page, PageSize: pageSize, Pages: pages, } } // Ensure compile-time interface compliance. var ( _ service.UserRepository = (*stubUserRepo)(nil) _ service.APIKeyRepository = (*stubApiKeyRepo)(nil) _ service.APIKeyCache = (*stubApiKeyCache)(nil) _ service.GroupRepository = (*stubGroupRepo)(nil) _ service.UserSubscriptionRepository = (*stubUserSubscriptionRepo)(nil) _ service.UsageLogRepository = (*stubUsageLogRepo)(nil) _ service.SettingRepository = (*stubSettingRepo)(nil) )