feat: backend service
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user