Files
galaxy-game/backend/internal/notification/deps.go
T
2026-05-06 10:14:55 +03:00

100 lines
3.6 KiB
Go

package notification
import (
"context"
"time"
"galaxy/backend/internal/config"
"galaxy/backend/internal/user"
"github.com/google/uuid"
"go.uber.org/zap"
)
// PushPublisher is the publisher contract notification uses to emit a
// `client_event` push frame to gateway. The real implementation lives
// in `backend/internal/push` ; NewNoopPushPublisher satisfies
// the interface for tests that do not exercise push behaviour.
//
// Implementations must be concurrency-safe. The deviceSessionID pointer
// narrows the event to a single device session when non-nil; nil means
// fan out to every active session of userID. eventID, requestID and
// traceID are correlation identifiers that gateway forwards verbatim
// into the signed client envelope; empty strings are forwarded
// unchanged.
type PushPublisher interface {
PublishClientEvent(ctx context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, kind string, payload map[string]any, eventID, requestID, traceID string) error
}
// Mailer is the email surface notification uses for outbound mail. The
// canonical implementation is `*mail.Service.EnqueueTemplate`; tests
// substitute a recording fake. The contract matches mail's existing
// signature so the wiring layer can pass the concrete service directly.
type Mailer interface {
EnqueueTemplate(ctx context.Context, templateID, recipient string, payload map[string]any, idempotencyKey string) error
}
// AccountResolver looks up the recipient profile (email + preferred
// language) by user_id. The canonical implementation is
// `*user.Service.GetAccount`. The narrow interface keeps the Service
// from depending on every part of the user surface.
type AccountResolver interface {
GetAccount(ctx context.Context, userID uuid.UUID) (user.Account, error)
}
// Deps aggregates every collaborator the Service depends on.
//
// Store, Mail, and Accounts must be non-nil. Push defaults to the no-op
// publisher when omitted; Now defaults to time.Now; Logger defaults to
// zap.NewNop. Config carries the worker interval, the max-attempts cap,
// and the optional admin-email destination from `BACKEND_NOTIFICATION_*`.
type Deps struct {
Store *Store
Mail Mailer
Push PushPublisher
Accounts AccountResolver
Config config.NotificationConfig
// Now overrides time.Now for deterministic tests. A nil Now defaults
// to time.Now in NewService.
Now func() time.Time
// Logger is named under "notification" by NewService. Nil falls back
// to zap.NewNop.
Logger *zap.Logger
}
// NewNoopPushPublisher returns a PushPublisher that logs every event
// at debug level and returns nil. The canonical publisher lives in
// `backend/internal/push`; this constructor exists for tests.
func NewNoopPushPublisher(logger *zap.Logger) PushPublisher {
if logger == nil {
logger = zap.NewNop()
}
return &noopPushPublisher{logger: logger.Named("push.noop")}
}
type noopPushPublisher struct {
logger *zap.Logger
}
func (p *noopPushPublisher) PublishClientEvent(_ context.Context, userID uuid.UUID, deviceSessionID *uuid.UUID, kind string, payload map[string]any, eventID, requestID, traceID string) error {
fields := []zap.Field{
zap.String("user_id", userID.String()),
zap.String("kind", kind),
zap.Int("payload_keys", len(payload)),
}
if deviceSessionID != nil {
fields = append(fields, zap.String("device_session_id", deviceSessionID.String()))
}
if eventID != "" {
fields = append(fields, zap.String("event_id", eventID))
}
if requestID != "" {
fields = append(fields, zap.String("request_id", requestID))
}
if traceID != "" {
fields = append(fields, zap.String("trace_id", traceID))
}
p.logger.Debug("client event (noop publisher)", fields...)
return nil
}