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