outbox for gateways

This commit is contained in:
Stephan D
2026-02-18 01:35:28 +01:00
parent 974caf286c
commit 69531cee73
221 changed files with 12172 additions and 782 deletions

View File

@@ -47,7 +47,7 @@ func Error[T any](logger mlogger.Logger, service mservice.Type, code codes.Code,
if err != nil {
fields = append(fields, zap.Error(err))
}
logger.Warn("gRPC request failed", fields...)
logger.Warn("GRPC request failed", fields...)
msg := message(err)
switch {

View File

@@ -204,7 +204,7 @@ func (r *Router) Start(ctx context.Context) error {
close(r.serveErr)
}()
r.logger.Info("gRPC server started", zap.String("network", r.listener.Addr().Network()), zap.String("address", r.listener.Addr().String()))
r.logger.Info("GRPC server started", zap.String("network", r.listener.Addr().Network()), zap.String("address", r.listener.Addr().String()))
return nil
}

View File

@@ -28,7 +28,7 @@ func Create(logger mlogger.Logger, db *mongo.Database) (*ChainAssetsDB, error) {
{Field: "asset.tokenSymbol", Sort: ri.Asc},
},
}); err != nil {
p.Logger.Error("failed index (chain, symbol) unique", zap.Error(err))
p.Logger.Error("Failed index (chain, symbol) unique", zap.Error(err))
return nil, err
}
@@ -42,7 +42,7 @@ func Create(logger mlogger.Logger, db *mongo.Database) (*ChainAssetsDB, error) {
{Field: "asset.contractAddress", Sort: ri.Asc},
},
}); err != nil {
p.Logger.Error("failed index (chain, contract) unique", zap.Error(err))
p.Logger.Error("Failed index (chain, contract) unique", zap.Error(err))
return nil, err
}
@@ -54,7 +54,7 @@ func Create(logger mlogger.Logger, db *mongo.Database) (*ChainAssetsDB, error) {
{Field: "asset.contractAddress", Sort: ri.Asc},
},
}); err != nil {
p.Logger.Error("failed index contract lookup", zap.Error(err))
p.Logger.Error("Failed index contract lookup", zap.Error(err))
return nil, err
}
@@ -65,7 +65,7 @@ func Create(logger mlogger.Logger, db *mongo.Database) (*ChainAssetsDB, error) {
{Field: "asset.chain", Sort: ri.Asc},
},
}); err != nil {
p.Logger.Error("failed index chain list", zap.Error(err))
p.Logger.Error("Failed index chain list", zap.Error(err))
return nil, err
}

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/pkg
go 1.24.0
go 1.25.0
require (
github.com/casbin/casbin/v2 v2.135.0
@@ -92,6 +92,6 @@ require (
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -271,8 +271,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -114,6 +114,42 @@ func NewNatsBroker(logger mlogger.Logger, settings *nc.Settings) (*NatsBroker, e
nats.Name(settings.NATSName),
nats.MaxReconnects(settings.MaxReconnects),
nats.ReconnectWait(time.Duration(settings.ReconnectWait) * time.Second),
nats.RetryOnFailedConnect(true),
nats.DisconnectErrHandler(func(conn *nats.Conn, err error) {
fields := []zap.Field{
zap.String("broker", settings.NATSName),
}
if conn != nil {
fields = append(fields, zap.String("connected_url", conn.ConnectedUrl()))
}
if err != nil {
fields = append(fields, zap.Error(err))
}
l.Warn("Disconnected from NATS", fields...)
}),
nats.ReconnectHandler(func(conn *nats.Conn) {
fields := []zap.Field{
zap.String("broker", settings.NATSName),
}
if conn != nil {
fields = append(fields, zap.String("connected_url", conn.ConnectedUrl()))
}
l.Info("Reconnected to NATS", fields...)
}),
nats.ClosedHandler(func(conn *nats.Conn) {
fields := []zap.Field{
zap.String("broker", settings.NATSName),
}
if conn != nil {
if url := conn.ConnectedUrl(); url != "" {
fields = append(fields, zap.String("connected_url", url))
}
if err := conn.LastError(); err != nil {
fields = append(fields, zap.Error(err))
}
}
l.Warn("NATS connection closed", fields...)
}),
}
if cfg != nil {
opts = append(opts, nats.UserInfo(cfg.User, cfg.Password))

View File

@@ -17,6 +17,7 @@ func (p *ChannelProducer) SendMessage(envelope me.Envelope) error {
// TODO: won't work with Kafka, need to serialize/deserialize
if err := p.broker.Publish(envelope); err != nil {
p.logger.Warn("Failed to publish message", zap.Error(err), mzap.Envelope(envelope))
return err
}
return nil
}

View File

@@ -1,7 +1,16 @@
package messaging
import me "github.com/tech/sendico/pkg/messaging/envelope"
import (
"context"
me "github.com/tech/sendico/pkg/messaging/envelope"
)
type Producer interface {
SendMessage(envelope me.Envelope) error
}
type ReliableProducer interface {
Producer
SendWithOutbox(ctx context.Context, envelope me.Envelope) error
}

View File

@@ -0,0 +1,26 @@
package reliable
import (
pmessaging "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
func NewReliableProducerFromConfig(logger mlogger.Logger, direct pmessaging.Producer, outbox OutboxStore, driverSettings model.SettingsT, opts ...Option) (*ReliableProducer, Settings, error) {
settings, err := ParseSettings(driverSettings)
if err != nil {
return nil, Settings{}, err
}
if !settings.Enabled {
return nil, settings, nil
}
combined := []Option{
WithBatchSize(settings.BatchSize),
WithPollInterval(settings.PollInterval()),
WithMaxAttempts(settings.MaxAttempts),
}
combined = append(combined, opts...)
return NewReliableProducer(logger, direct, outbox, combined...), settings, nil
}

View File

@@ -0,0 +1,28 @@
package reliable
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
func TestNewReliableProducerFromConfigUsesDefaults(t *testing.T) {
producer, settings, err := NewReliableProducerFromConfig(zap.NewNop(), &recordingDirectProducer{}, &recordingStore{}, model.SettingsT{})
require.NoError(t, err)
require.NotNil(t, producer)
assert.Equal(t, DefaultSettings(), settings)
}
func TestNewReliableProducerFromConfigCanDisable(t *testing.T) {
producer, settings, err := NewReliableProducerFromConfig(zap.NewNop(), &recordingDirectProducer{}, &recordingStore{}, model.SettingsT{
SettingsBlockKey: map[string]any{
"enabled": false,
},
})
require.NoError(t, err)
assert.Nil(t, producer)
assert.False(t, settings.Enabled)
}

View File

@@ -0,0 +1,238 @@
package reliable
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/tech/sendico/pkg/merrors"
pmessaging "github.com/tech/sendico/pkg/messaging"
me "github.com/tech/sendico/pkg/messaging/envelope"
"github.com/tech/sendico/pkg/mlogger"
"go.uber.org/zap"
)
const (
defaultBatchSize = 100
defaultPollInterval = time.Second
defaultMaxAttempts = 5
)
type OutboxMessage struct {
Reference string
EventID string
Subject string
Payload []byte
Attempts int
OrganizationRef string
CreatedAt time.Time
}
type OutboxStore interface {
Enqueue(ctx context.Context, msg OutboxMessage) error
ListPending(ctx context.Context, limit int) ([]OutboxMessage, error)
MarkSent(ctx context.Context, reference string, sentAt time.Time) error
MarkFailed(ctx context.Context, reference string) error
IncrementAttempts(ctx context.Context, reference string) error
}
type EnvelopeDecoder func(record OutboxMessage) (me.Envelope, error)
type Option func(*ReliableProducer)
type ReliableProducer struct {
logger mlogger.Logger
direct pmessaging.Producer
outbox OutboxStore
batchSize int
pollInterval time.Duration
maxAttempts int
decode EnvelopeDecoder
}
func NewReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, outbox OutboxStore, opts ...Option) *ReliableProducer {
if logger == nil {
logger = zap.NewNop()
}
res := &ReliableProducer{
logger: logger.Named("reliable_producer"),
direct: direct,
outbox: outbox,
batchSize: defaultBatchSize,
pollInterval: defaultPollInterval,
maxAttempts: defaultMaxAttempts,
decode: defaultEnvelopeDecoder,
}
for _, opt := range opts {
if opt != nil {
opt(res)
}
}
return res
}
func WithBatchSize(size int) Option {
return func(p *ReliableProducer) {
if size > 0 {
p.batchSize = size
}
}
}
func WithPollInterval(interval time.Duration) Option {
return func(p *ReliableProducer) {
if interval > 0 {
p.pollInterval = interval
}
}
}
func WithMaxAttempts(maxAttempts int) Option {
return func(p *ReliableProducer) {
p.maxAttempts = maxAttempts
}
}
func WithEnvelopeDecoder(decoder EnvelopeDecoder) Option {
return func(p *ReliableProducer) {
if decoder != nil {
p.decode = decoder
}
}
}
func (p *ReliableProducer) SendMessage(envelope me.Envelope) error {
if p == nil {
return merrors.Internal("reliable producer is nil")
}
if p.direct == nil {
return merrors.Internal("reliable producer direct publisher is not configured")
}
return p.direct.SendMessage(envelope)
}
func (p *ReliableProducer) SendWithOutbox(ctx context.Context, envelope me.Envelope) error {
if p == nil {
return merrors.Internal("reliable producer is nil")
}
if envelope == nil {
return merrors.InvalidArgument("envelope is required")
}
if p.outbox == nil {
return merrors.Internal("reliable producer outbox store is not configured")
}
data, err := envelope.Serialize()
if err != nil {
return err
}
eventID := envelope.GetMessageId().String()
if _, err = uuid.Parse(eventID); err != nil {
return merrors.InvalidArgument("envelope message id is invalid")
}
return p.outbox.Enqueue(ctx, OutboxMessage{
EventID: eventID,
Subject: envelope.GetSignature().ToString(),
Payload: data,
})
}
func (p *ReliableProducer) Run(ctx context.Context) {
if p == nil {
return
}
if p.outbox == nil {
p.logger.Warn("Outbox dispatcher disabled: store is not configured")
return
}
if p.direct == nil {
p.logger.Warn("Outbox dispatcher disabled: direct producer is not configured")
return
}
p.logger.Info("Outbox dispatcher started")
defer p.logger.Info("Outbox dispatcher stopped")
for {
if ctx.Err() != nil {
return
}
processed, err := p.DispatchPending(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
p.logger.Warn("Failed to dispatch outbox events", zap.Error(err))
}
if processed == 0 {
select {
case <-ctx.Done():
return
case <-time.After(p.pollInterval):
}
}
}
}
func (p *ReliableProducer) DispatchPending(ctx context.Context) (int, error) {
if p == nil || p.outbox == nil || p.direct == nil {
return 0, nil
}
events, err := p.outbox.ListPending(ctx, p.batchSize)
if err != nil {
return 0, err
}
for _, event := range events {
if ctx.Err() != nil {
return len(events), ctx.Err()
}
env, decodeErr := p.decode(event)
if decodeErr != nil {
p.logger.Warn("Failed to decode outbox envelope", zap.String("event_id", event.EventID), zap.Error(decodeErr))
p.handleFailure(ctx, event, decodeErr)
continue
}
if sendErr := p.direct.SendMessage(env); sendErr != nil {
p.logger.Warn("Failed to publish outbox event", zap.String("event_id", event.EventID), zap.String("subject", event.Subject), zap.Error(sendErr))
p.handleFailure(ctx, event, sendErr)
continue
}
if markErr := p.outbox.MarkSent(ctx, event.Reference, time.Now().UTC()); markErr != nil {
p.logger.Warn("Failed to mark outbox event sent", zap.String("event_id", event.EventID), zap.String("reference", event.Reference), zap.Error(markErr))
}
}
return len(events), nil
}
func (p *ReliableProducer) handleFailure(ctx context.Context, event OutboxMessage, _ error) {
if p == nil || p.outbox == nil {
return
}
if event.Reference == "" {
p.logger.Warn("Cannot record outbox failure: missing record reference", zap.String("event_id", event.EventID))
return
}
if err := p.outbox.IncrementAttempts(ctx, event.Reference); err != nil && !errors.Is(err, context.Canceled) {
p.logger.Warn("Failed to increment outbox attempts", zap.String("event_id", event.EventID), zap.String("reference", event.Reference), zap.Error(err))
}
if p.maxAttempts > 0 && event.Attempts+1 >= p.maxAttempts {
if err := p.outbox.MarkFailed(ctx, event.Reference); err != nil && !errors.Is(err, context.Canceled) {
p.logger.Warn("Failed to mark outbox event failed", zap.String("event_id", event.EventID), zap.String("reference", event.Reference), zap.Error(err))
}
}
}
func defaultEnvelopeDecoder(record OutboxMessage) (me.Envelope, error) {
return me.Deserialize(record.Payload)
}
var _ pmessaging.ReliableProducer = (*ReliableProducer)(nil)

View File

@@ -0,0 +1,164 @@
package reliable
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
me "github.com/tech/sendico/pkg/messaging/envelope"
domainmodel "github.com/tech/sendico/pkg/model"
notification "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
"go.uber.org/zap"
)
func TestReliableProducerSendWithOutbox(t *testing.T) {
store := &recordingStore{}
producer := NewReliableProducer(zap.NewNop(), nil, store)
env := me.CreateEnvelope("test-sender", domainmodel.NewNotification(mservice.Payments, notification.NACreated))
_, err := env.Wrap([]byte(`{"ok":true}`))
require.NoError(t, err)
err = producer.SendWithOutbox(context.Background(), env)
require.NoError(t, err)
require.Len(t, store.enqueued, 1)
record := store.enqueued[0]
assert.Equal(t, env.GetMessageId().String(), record.EventID)
assert.Equal(t, "payments_created", record.Subject)
decoded, err := me.Deserialize(record.Payload)
require.NoError(t, err)
assert.Equal(t, env.GetMessageId(), decoded.GetMessageId())
assert.Equal(t, env.GetSignature().ToString(), decoded.GetSignature().ToString())
}
func TestReliableProducerDispatchPendingSuccess(t *testing.T) {
env := me.CreateEnvelope("test-sender", domainmodel.NewNotification(mservice.Payments, notification.NAUpdated))
_, err := env.Wrap([]byte(`{"event":"updated"}`))
require.NoError(t, err)
payload, err := env.Serialize()
require.NoError(t, err)
store := &recordingStore{
pending: []OutboxMessage{
{
Reference: "ref-1",
EventID: env.GetMessageId().String(),
Subject: env.GetSignature().ToString(),
Payload: payload,
Attempts: 0,
},
},
}
direct := &recordingDirectProducer{}
producer := NewReliableProducer(zap.NewNop(), direct, store)
processed, err := producer.DispatchPending(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, processed)
require.Len(t, direct.sent, 1)
assert.Equal(t, env.GetMessageId(), direct.sent[0].GetMessageId())
require.Len(t, store.markedSent, 1)
assert.Equal(t, "ref-1", store.markedSent[0])
assert.Empty(t, store.incremented)
assert.Empty(t, store.markedFailed)
}
func TestReliableProducerDispatchPendingFailureMarksFailed(t *testing.T) {
env := me.CreateEnvelope("test-sender", domainmodel.NewNotification(mservice.Payments, notification.NAUpdated))
_, err := env.Wrap([]byte(`{"event":"updated"}`))
require.NoError(t, err)
payload, err := env.Serialize()
require.NoError(t, err)
store := &recordingStore{
pending: []OutboxMessage{
{
Reference: "ref-2",
EventID: env.GetMessageId().String(),
Subject: env.GetSignature().ToString(),
Payload: payload,
Attempts: 4,
},
},
}
direct := &recordingDirectProducer{err: errors.New("publish failed")}
producer := NewReliableProducer(zap.NewNop(), direct, store, WithMaxAttempts(5))
processed, err := producer.DispatchPending(context.Background())
require.NoError(t, err)
assert.Equal(t, 1, processed)
require.Len(t, store.incremented, 1)
assert.Equal(t, "ref-2", store.incremented[0])
require.Len(t, store.markedFailed, 1)
assert.Equal(t, "ref-2", store.markedFailed[0])
assert.Empty(t, store.markedSent)
}
type recordingStore struct {
mu sync.Mutex
enqueued []OutboxMessage
pending []OutboxMessage
markedSent []string
markedFailed []string
incremented []string
}
func (s *recordingStore) Enqueue(_ context.Context, msg OutboxMessage) error {
s.mu.Lock()
defer s.mu.Unlock()
s.enqueued = append(s.enqueued, msg)
return nil
}
func (s *recordingStore) ListPending(_ context.Context, _ int) ([]OutboxMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
events := append([]OutboxMessage(nil), s.pending...)
s.pending = nil
return events, nil
}
func (s *recordingStore) MarkSent(_ context.Context, reference string, _ time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
s.markedSent = append(s.markedSent, reference)
return nil
}
func (s *recordingStore) MarkFailed(_ context.Context, reference string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.markedFailed = append(s.markedFailed, reference)
return nil
}
func (s *recordingStore) IncrementAttempts(_ context.Context, reference string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.incremented = append(s.incremented, reference)
return nil
}
type recordingDirectProducer struct {
mu sync.Mutex
sent []me.Envelope
err error
}
func (p *recordingDirectProducer) SendMessage(env me.Envelope) error {
p.mu.Lock()
defer p.mu.Unlock()
p.sent = append(p.sent, env)
return p.err
}

View File

@@ -0,0 +1,64 @@
package reliable
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/tech/sendico/pkg/model"
)
const SettingsBlockKey = "reliable_publisher"
type Settings struct {
Enabled bool `mapstructure:"enabled" yaml:"enabled"`
BatchSize int `mapstructure:"batch_size" yaml:"batch_size"`
PollIntervalSeconds int `mapstructure:"poll_interval_seconds" yaml:"poll_interval_seconds"`
MaxAttempts int `mapstructure:"max_attempts" yaml:"max_attempts"`
}
func DefaultSettings() Settings {
return Settings{
Enabled: true,
BatchSize: defaultBatchSize,
PollIntervalSeconds: int(defaultPollInterval.Seconds()),
MaxAttempts: defaultMaxAttempts,
}
}
func ParseSettings(driverSettings model.SettingsT) (Settings, error) {
settings := DefaultSettings()
if len(driverSettings) == 0 {
return settings, nil
}
raw, ok := driverSettings[SettingsBlockKey]
if !ok || raw == nil {
return settings, nil
}
if err := mapstructure.Decode(raw, &settings); err != nil {
return Settings{}, err
}
settings.applyDefaults()
return settings, nil
}
func (s *Settings) PollInterval() time.Duration {
if s == nil || s.PollIntervalSeconds <= 0 {
return defaultPollInterval
}
return time.Duration(s.PollIntervalSeconds) * time.Second
}
func (s *Settings) applyDefaults() {
if s.BatchSize <= 0 {
s.BatchSize = defaultBatchSize
}
if s.PollIntervalSeconds <= 0 {
s.PollIntervalSeconds = int(defaultPollInterval.Seconds())
}
if s.MaxAttempts <= 0 {
s.MaxAttempts = defaultMaxAttempts
}
}

View File

@@ -0,0 +1,62 @@
package reliable
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tech/sendico/pkg/model"
)
func TestParseSettingsDefaultsWhenBlockMissing(t *testing.T) {
got, err := ParseSettings(model.SettingsT{
"url_env": "NATS_URL",
})
require.NoError(t, err)
assert.Equal(t, DefaultSettings(), got)
}
func TestParseSettingsOverrides(t *testing.T) {
got, err := ParseSettings(model.SettingsT{
SettingsBlockKey: map[string]any{
"enabled": true,
"batch_size": 250,
"poll_interval_seconds": 3,
"max_attempts": 7,
},
})
require.NoError(t, err)
assert.True(t, got.Enabled)
assert.Equal(t, 250, got.BatchSize)
assert.Equal(t, 3, got.PollIntervalSeconds)
assert.Equal(t, 7, got.MaxAttempts)
}
func TestParseSettingsAppliesDefaultsForInvalidNumbers(t *testing.T) {
got, err := ParseSettings(model.SettingsT{
SettingsBlockKey: map[string]any{
"enabled": true,
"batch_size": 0,
"poll_interval_seconds": -1,
"max_attempts": 0,
},
})
require.NoError(t, err)
assert.True(t, got.Enabled)
assert.Equal(t, defaultBatchSize, got.BatchSize)
assert.Equal(t, int(defaultPollInterval.Seconds()), got.PollIntervalSeconds)
assert.Equal(t, defaultMaxAttempts, got.MaxAttempts)
}
func TestParseSettingsCanDisableReliablePublisher(t *testing.T) {
got, err := ParseSettings(model.SettingsT{
SettingsBlockKey: map[string]any{
"enabled": false,
},
})
require.NoError(t, err)
assert.False(t, got.Enabled)
}

View File

@@ -80,6 +80,7 @@ func StringToNotificationAction(s string) (nm.NotificationAction, error) {
nm.NAUpdated,
nm.NAArchived,
nm.NADeleted,
nm.NASent,
nm.NAAssigned,
nm.NAPasswordReset,
nm.NAConfirmationRequest,

View File

@@ -0,0 +1,54 @@
package types
// QuoteExecutionReadiness classifies whether execution is immediately possible.
type QuoteExecutionReadiness string
const (
QuoteExecutionReadinessUnspecified QuoteExecutionReadiness = "unspecified"
QuoteExecutionReadinessLiquidityReady QuoteExecutionReadiness = "liquidity_ready"
QuoteExecutionReadinessLiquidityObtainable QuoteExecutionReadiness = "liquidity_obtainable"
QuoteExecutionReadinessIndicative QuoteExecutionReadiness = "indicative"
)
type QuoteRouteHopRole string
const (
QuoteRouteHopRoleUnspecified QuoteRouteHopRole = "unspecified"
QuoteRouteHopRoleSource QuoteRouteHopRole = "source"
QuoteRouteHopRoleTransit QuoteRouteHopRole = "transit"
QuoteRouteHopRoleDestination QuoteRouteHopRole = "destination"
)
type QuoteRouteHop struct {
Index uint32 `bson:"index,omitempty" json:"index,omitempty"`
Rail string `bson:"rail,omitempty" json:"rail,omitempty"`
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
Network string `bson:"network,omitempty" json:"network,omitempty"`
Role QuoteRouteHopRole `bson:"role,omitempty" json:"role,omitempty"`
}
// QuoteRouteSpecification is an abstract route selected during quotation.
// It intentionally omits execution steps/operations.
type QuoteRouteSpecification struct {
Rail string `bson:"rail,omitempty" json:"rail,omitempty"`
Provider string `bson:"provider,omitempty" json:"provider,omitempty"`
PayoutMethod string `bson:"payoutMethod,omitempty" json:"payoutMethod,omitempty"`
SettlementAsset string `bson:"settlementAsset,omitempty" json:"settlementAsset,omitempty"`
SettlementModel string `bson:"settlementModel,omitempty" json:"settlementModel,omitempty"`
Network string `bson:"network,omitempty" json:"network,omitempty"`
RouteRef string `bson:"routeRef,omitempty" json:"routeRef,omitempty"`
PricingProfileRef string `bson:"pricingProfileRef,omitempty" json:"pricingProfileRef,omitempty"`
Hops []*QuoteRouteHop `bson:"hops,omitempty" json:"hops,omitempty"`
}
// QuoteExecutionConditions stores quotation-time assumptions and constraints.
type QuoteExecutionConditions struct {
Readiness QuoteExecutionReadiness `bson:"readiness,omitempty" json:"readiness,omitempty"`
BatchingEligible bool `bson:"batchingEligible,omitempty" json:"batchingEligible,omitempty"`
PrefundingRequired bool `bson:"prefundingRequired,omitempty" json:"prefundingRequired,omitempty"`
PrefundingCostIncluded bool `bson:"prefundingCostIncluded,omitempty" json:"prefundingCostIncluded,omitempty"`
LiquidityCheckRequiredAtExecution bool `bson:"liquidityCheckRequiredAtExecution,omitempty" json:"liquidityCheckRequiredAtExecution,omitempty"`
LatencyHint string `bson:"latencyHint,omitempty" json:"latencyHint,omitempty"`
Assumptions []string `bson:"assumptions,omitempty" json:"assumptions,omitempty"`
}

View File

@@ -184,10 +184,10 @@ func (a *App[T]) Start() error {
a.cleanup(context.Background())
return err
}
a.logger.Debug("gRPC services registered")
a.logger.Debug("GRPC services registered")
a.runCtx, a.cancel = context.WithCancel(context.Background())
a.logger.Debug("gRPC server context initialised")
a.logger.Debug("GRPC server context initialised")
if err := a.grpc.Start(a.runCtx); err != nil {
a.logger.Error("Failed to start gRPC server", zap.Error(err))
@@ -210,9 +210,9 @@ func (a *App[T]) Start() error {
err = <-a.grpc.Done()
if err != nil && !errors.Is(err, context.Canceled) {
a.logger.Error("gRPC server stopped with error", zap.Error(err))
a.logger.Error("GRPC server stopped with error", zap.Error(err))
} else {
a.logger.Info("gRPC server finished")
a.logger.Info("GRPC server finished")
}
a.cleanup(context.Background())
@@ -230,7 +230,7 @@ func (a *App[T]) Shutdown(ctx context.Context) {
if err := a.grpc.Finish(ctx); err != nil && !errors.Is(err, context.Canceled) {
a.logger.Warn("Failed to stop gRPC server gracefully", zap.Error(err))
} else {
a.logger.Info("gRPC server stopped")
a.logger.Info("GRPC server stopped")
}
}
a.cleanup(ctx)