Files
galaxy-game/backend/internal/lobby/racenames_register.go
T
2026-05-06 10:14:55 +03:00

102 lines
3.5 KiB
Go

package lobby
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"go.uber.org/zap"
)
// RegisterRaceName promotes a `pending_registration` row owned by
// userID into a `registered` row. The promotion succeeds when:
//
// - the user has a `pending_registration` row matching the supplied
// display name (canonical key);
// - the row is still inside its 30-day window (expires_at > now);
// - the user owns fewer than `entitlement.max_registered_race_names`
// `registered` rows.
func (s *Service) RegisterRaceName(ctx context.Context, userID uuid.UUID, displayName string) (RaceNameEntry, error) {
displayName, err := ValidateDisplayName(displayName)
if err != nil {
return RaceNameEntry{}, err
}
canonical, err := s.deps.Policy.Canonical(displayName)
if err != nil {
return RaceNameEntry{}, err
}
rows, err := s.deps.Store.FindRaceNameByCanonical(ctx, canonical)
if err != nil {
return RaceNameEntry{}, err
}
var pending *RaceNameEntry
for i := range rows {
row := rows[i]
if row.OwnerUserID != userID {
if row.Status == RaceNameStatusRegistered ||
row.Status == RaceNameStatusReservation ||
row.Status == RaceNameStatusPendingRegistration {
return RaceNameEntry{}, fmt.Errorf("%w: race name held by another user", ErrRaceNameTaken)
}
continue
}
if row.Status == RaceNameStatusRegistered {
return RaceNameEntry{}, fmt.Errorf("%w: race name already registered by caller", ErrConflict)
}
if row.Status == RaceNameStatusPendingRegistration {
pending = &rows[i]
}
}
if pending == nil {
return RaceNameEntry{}, fmt.Errorf("%w: no pending_registration row for caller", ErrNotFound)
}
now := s.deps.Now().UTC()
if pending.ExpiresAt != nil && !pending.ExpiresAt.After(now) {
return RaceNameEntry{}, fmt.Errorf("%w: pending_registration window closed at %s", ErrPendingExpired, pending.ExpiresAt.UTC().Format("2006-01-02T15:04:05Z07:00"))
}
maxAllowed := int32(1)
if s.deps.Entitlement != nil {
got, eerr := s.deps.Entitlement.GetMaxRegisteredRaceNames(ctx, userID)
if eerr != nil {
return RaceNameEntry{}, fmt.Errorf("lobby: read entitlement: %w", eerr)
}
maxAllowed = got
}
currentCount, err := s.deps.Store.CountRegisteredRaceNamesByUser(ctx, userID)
if err != nil {
return RaceNameEntry{}, err
}
if int32(currentCount) >= maxAllowed {
return RaceNameEntry{}, fmt.Errorf("%w: %d registered race names of %d allowed", ErrEntitlementExceeded, currentCount, maxAllowed)
}
entry, err := s.deps.Store.PromotePendingToRegistered(ctx, canonical, userID, pending.GameID, displayName, now)
if err != nil {
if errors.Is(err, ErrNotFound) {
return RaceNameEntry{}, fmt.Errorf("%w: pending row vanished concurrently", ErrConflict)
}
return RaceNameEntry{}, err
}
s.deps.Cache.RemoveRaceName(canonical)
s.deps.Cache.PutRaceName(entry)
intent := LobbyNotification{
Kind: NotificationLobbyRaceNameRegistered,
IdempotencyKey: "racename-registered:" + string(canonical),
Recipients: []uuid.UUID{userID},
Payload: map[string]any{
"race_name": displayName,
},
}
if pubErr := s.deps.Notification.PublishLobbyEvent(ctx, intent); pubErr != nil {
s.deps.Logger.Warn("race-name registered notification failed",
zap.String("canonical", string(canonical)),
zap.Error(pubErr))
}
return entry, nil
}
// ListMyRaceNames returns every race-name row owned by userID.
func (s *Service) ListMyRaceNames(ctx context.Context, userID uuid.UUID) ([]RaceNameEntry, error) {
return s.deps.Store.ListRaceNamesForUser(ctx, userID)
}