package diplomail import ( "context" "time" "galaxy/backend/internal/config" "github.com/google/uuid" "go.uber.org/zap" ) // Deps aggregates every collaborator the diplomail Service depends on. // // Store and Memberships are required. Logger and Now default to // zap.NewNop / time.Now when nil. Notification falls back to a no-op // publisher so unit tests can construct a Service with only the // required collaborators populated. type Deps struct { Store *Store Memberships MembershipLookup Notification NotificationPublisher Config config.DiplomailConfig Logger *zap.Logger Now func() time.Time } // ActiveMembership is the slim view of a single (user, game) roster // row the diplomail package needs at send time: it confirms the // participant is active in the game and captures the snapshot fields // (`game_name`, `user_name`, `race_name`) that we persist on each new // message / recipient row. type ActiveMembership struct { UserID uuid.UUID GameID uuid.UUID GameName string UserName string RaceName string } // MembershipLookup is the read-only surface diplomail uses to verify // "is this user an active member of this game" and to snapshot the // roster metadata. The canonical implementation in `cmd/backend/main` // adapts the `*lobby.Service` membership cache to this interface. // // Implementations must return ErrNotFound (the diplomail sentinel) // when the user is not an active member of the game; the service // boundary maps that to 403 forbidden. type MembershipLookup interface { GetActiveMembership(ctx context.Context, gameID, userID uuid.UUID) (ActiveMembership, error) } // NotificationPublisher is the outbound surface diplomail uses to // emit the `diplomail.message.received` push event. The canonical // implementation in `cmd/backend/main` adapts the notification.Service // the same way it adapts `lobby.NotificationPublisher`; tests pass // the no-op publisher below to avoid wiring the dispatcher. type NotificationPublisher interface { PublishDiplomailEvent(ctx context.Context, ev DiplomailNotification) error } // DiplomailNotification is the open shape carried by a per-recipient // push intent. The struct lives in the diplomail package so the // producer vocabulary stays here; the publisher adapter translates it // into a `notification.Intent` at the wiring boundary. type DiplomailNotification struct { Kind string IdempotencyKey string Recipient uuid.UUID Payload map[string]any } // NewNoopNotificationPublisher returns a publisher that logs every // call at debug level and returns nil. Used by unit tests and as the // fallback inside NewService when callers leave Deps.Notification nil. func NewNoopNotificationPublisher(logger *zap.Logger) NotificationPublisher { if logger == nil { logger = zap.NewNop() } return &noopNotificationPublisher{logger: logger.Named("diplomail.notify.noop")} } type noopNotificationPublisher struct { logger *zap.Logger } func (p *noopNotificationPublisher) PublishDiplomailEvent(_ context.Context, ev DiplomailNotification) error { p.logger.Debug("noop notification", zap.String("kind", ev.Kind), zap.String("idempotency_key", ev.IdempotencyKey), zap.String("recipient", ev.Recipient.String()), ) return nil }