package lobby import ( "context" "errors" "fmt" "github.com/google/uuid" "go.uber.org/zap" ) // OnUserBlocked releases every lobby binding owned by the user under // the `blocked` semantics: active memberships flip to `blocked`, // pending applications get rejected, pending invites incoming get // declined / outgoing get revoked, race-name entries are deleted, and // owned games in non-running statuses are cancelled. // // Implements `internal/user.LobbyCascade.OnUserBlocked`. Errors during // the cascade are joined and returned but never roll back the // already-committed user write — the canonical state is the row in // Postgres. func (s *Service) OnUserBlocked(ctx context.Context, userID uuid.UUID) error { return s.runCascade(ctx, userID, MembershipStatusBlocked) } // OnUserDeleted runs the same cascade as OnUserBlocked but transitions // memberships to `removed` instead of `blocked`. Implements // `internal/user.LobbyCascade.OnUserDeleted`. func (s *Service) OnUserDeleted(ctx context.Context, userID uuid.UUID) error { return s.runCascade(ctx, userID, MembershipStatusRemoved) } func (s *Service) runCascade(ctx context.Context, userID uuid.UUID, membershipStatus string) error { snap, err := s.deps.Store.LoadCascadeSnapshot(ctx, userID) if err != nil { return fmt.Errorf("lobby cascade: load snapshot: %w", err) } if snap.empty() { return nil } now := s.deps.Now().UTC() if err := s.deps.Store.CascadeUser(ctx, userID, snap, membershipStatus, now); err != nil { return fmt.Errorf("lobby cascade: write: %w", err) } s.deps.Cache.EvictUserMemberships(userID) s.deps.Cache.EvictUserRaceNames(userID) s.deps.Cache.EvictOwnerGames(userID) var notifyErrs []error for _, gameID := range snap.OwnedGameIDs { s.deps.Cache.RemoveGame(gameID) } if len(snap.ActiveMembershipIDs) > 0 { intent := LobbyNotification{ Kind: NotificationLobbyMembershipRemoved, IdempotencyKey: "user-cascade-membership:" + userID.String(), Recipients: []uuid.UUID{userID}, Payload: map[string]any{ "reason": membershipStatus, }, } if pubErr := s.deps.Notification.PublishLobbyEvent(ctx, intent); pubErr != nil { notifyErrs = append(notifyErrs, pubErr) } } if len(notifyErrs) > 0 { s.deps.Logger.Warn("lobby cascade notification failures", zap.String("user_id", userID.String()), zap.Int("notify_errors", len(notifyErrs))) } return errors.Join(notifyErrs...) } func (snap CascadeUserSnapshot) empty() bool { return len(snap.OwnedGameIDs) == 0 && len(snap.ActiveMembershipIDs) == 0 && len(snap.PendingApplications) == 0 && len(snap.IncomingInvites) == 0 && len(snap.OutgoingInvites) == 0 && len(snap.RaceNameKeys) == 0 }