// Package racenameintents adapts the per-game capability evaluator's // RaceNameIntents interface to the shared galaxy/notificationintent // publisher. introduced a NoopRaceNameIntents shim while the // notification catalog lacked the lobby.race_name.* types; lands // those types and this adapter replaces the shim in production wiring. package racenameintents import ( "context" "errors" "fmt" "log/slog" "time" "galaxy/lobby/internal/ports" "galaxy/lobby/internal/service/capabilityevaluation" "galaxy/notificationintent" ) // Publisher implements capabilityevaluation.RaceNameIntents by composing // the type-specific notificationintent constructors with the shared // IntentPublisher port. type Publisher struct { publisher ports.IntentPublisher clock func() time.Time logger *slog.Logger } // Config groups the dependencies required to construct a Publisher. type Config struct { // Publisher receives every constructed notification intent. The // adapter never falls back to a noop; transport errors are wrapped // and returned so the evaluator's logging path can record them. Publisher ports.IntentPublisher // Clock supplies the wall-clock used for log timestamps. The // adapter copies FinishedAt from the inbound event into the intent // metadata, so the clock is currently unused inside Publish*; it is // retained on the struct for parity with other lobby adapters and // for forthcoming tracing hooks. Clock func() time.Time // Logger receives optional adapter-level structured logs. Defaults // to slog.Default() if nil. Logger *slog.Logger } // NewPublisher constructs one Publisher. func NewPublisher(cfg Config) (*Publisher, error) { if cfg.Publisher == nil { return nil, errors.New("new race name intents publisher: nil intent publisher") } clock := cfg.Clock if clock == nil { clock = time.Now } logger := cfg.Logger if logger == nil { logger = slog.Default() } return &Publisher{ publisher: cfg.Publisher, clock: clock, logger: logger.With("adapter", "lobby.racenameintents"), }, nil } // PublishEligible builds a lobby.race_name.registration_eligible intent // from ev and forwards it to the underlying intent publisher. Idempotency // is scoped by (game_id, user_id) so retries of the same evaluator pass // collapse to a single notification at the consumer. func (publisher *Publisher) PublishEligible(ctx context.Context, ev capabilityevaluation.EligibleEvent) error { if publisher == nil { return errors.New("publish race name eligible intent: nil publisher") } if ctx == nil { return errors.New("publish race name eligible intent: nil context") } gameID := ev.GameID.String() intent, err := notificationintent.NewLobbyRaceNameRegistrationEligibleIntent( notificationintent.Metadata{ IdempotencyKey: "game-lobby:race-name-eligible:" + gameID + ":" + ev.UserID, OccurredAt: ev.FinishedAt, }, ev.UserID, notificationintent.LobbyRaceNameRegistrationEligiblePayload{ GameID: gameID, GameName: ev.GameName, RaceName: ev.RaceName, EligibleUntilMs: ev.EligibleUntil.UnixMilli(), }, ) if err != nil { return fmt.Errorf("publish race name eligible intent: build intent: %w", err) } if _, err := publisher.publisher.Publish(ctx, intent); err != nil { return fmt.Errorf("publish race name eligible intent: %w", err) } return nil } // PublishDenied builds a lobby.race_name.registration_denied intent from // ev and forwards it to the underlying intent publisher. func (publisher *Publisher) PublishDenied(ctx context.Context, ev capabilityevaluation.DeniedEvent) error { if publisher == nil { return errors.New("publish race name denied intent: nil publisher") } if ctx == nil { return errors.New("publish race name denied intent: nil context") } gameID := ev.GameID.String() intent, err := notificationintent.NewLobbyRaceNameRegistrationDeniedIntent( notificationintent.Metadata{ IdempotencyKey: "game-lobby:race-name-denied:" + gameID + ":" + ev.UserID, OccurredAt: ev.FinishedAt, }, ev.UserID, notificationintent.LobbyRaceNameRegistrationDeniedPayload{ GameID: gameID, GameName: ev.GameName, RaceName: ev.RaceName, Reason: ev.Reason, }, ) if err != nil { return fmt.Errorf("publish race name denied intent: build intent: %w", err) } if _, err := publisher.publisher.Publish(ctx, intent); err != nil { return fmt.Errorf("publish race name denied intent: %w", err) } return nil } // Compile-time interface assertion. var _ capabilityevaluation.RaceNameIntents = (*Publisher)(nil)