102 lines
3.5 KiB
Go
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)
|
|
}
|