diff --git a/internal/userbilling/service.go b/internal/userbilling/service.go index 798b0912..906da2b3 100644 --- a/internal/userbilling/service.go +++ b/internal/userbilling/service.go @@ -1220,6 +1220,7 @@ LIMIT $1`, limit) defer rows.Close() users := []adminBillingUserSummary{} + userIDs := []string{} for rows.Next() { var item adminBillingUserSummary var lastUsed sql.NullTime @@ -1236,19 +1237,61 @@ LIMIT $1`, limit) item.APIKeys.LastUsedAt = &value } item.Credits.Available = item.Credits.Balance - item.Credits.Reserved - reservations, err := s.activeReservations(ctx, item.User.ID, 5) - if err != nil { - return nil, err - } - item.Reservations.Active = reservations + userIDs = append(userIDs, item.User.ID) users = append(users, item) } if err := rows.Err(); err != nil { return nil, err } + reservations, err := s.activeReservationsForUsers(ctx, userIDs, 5) + if err != nil { + return nil, err + } + for i := range users { + users[i].Reservations.Active = reservations[users[i].User.ID] + } return users, nil } +func (s *Server) activeReservationsForUsers(ctx context.Context, userIDs []string, perUserLimit int) (map[string][]reservationSummaryRow, error) { + items := map[string][]reservationSummaryRow{} + if len(userIDs) == 0 { + return items, nil + } + placeholders := make([]string, len(userIDs)) + args := make([]any, 0, len(userIDs)+1) + for i, userID := range userIDs { + placeholders[i] = "$" + strconv.Itoa(i+1) + args = append(args, userID) + } + limitParam := "$" + strconv.Itoa(len(args)+1) + args = append(args, perUserLimit) + query := ` +SELECT user_id, id, service_id, estimated_credits, estimated_tokens, status, expires_at, created_at +FROM ( + SELECT user_id, id, service_id, estimated_credits, estimated_tokens, status, expires_at, created_at, + row_number() OVER (PARTITION BY user_id ORDER BY expires_at ASC, created_at DESC) AS rn + FROM hwlab_billing_reservations + WHERE user_id IN (` + strings.Join(placeholders, ", ") + `) AND status = 'reserved' AND expires_at > now() +) ranked +WHERE rn <= ` + limitParam + ` +ORDER BY expires_at ASC, created_at DESC` + rows, err := s.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var userID string + var item reservationSummaryRow + if err := rows.Scan(&userID, &item.ReservationID, &item.ServiceID, &item.EstimatedCredits, &item.EstimatedTokens, &item.Status, &item.ExpiresAt, &item.CreatedAt); err != nil { + return nil, err + } + items[userID] = append(items[userID], item) + } + return items, rows.Err() +} + func (s *Server) adminBillingTotals(ctx context.Context) (map[string]any, error) { var totals struct { Users int64