outbox for gateways
This commit is contained in:
@@ -4,10 +4,13 @@ go 1.25.7
|
||||
|
||||
replace github.com/tech/sendico/pkg => ../../pkg
|
||||
|
||||
replace github.com/tech/sendico/gateway/common => ../common
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/tech/sendico/gateway/common v0.1.0
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
go.uber.org/zap v1.27.1
|
||||
@@ -48,5 +51,5 @@ require (
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.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
|
||||
)
|
||||
|
||||
@@ -210,8 +210,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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/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=
|
||||
|
||||
@@ -191,14 +191,18 @@ func (i *Imp) Start() error {
|
||||
if cfg.GRPC != nil {
|
||||
invokeURI = cfg.GRPC.DiscoveryInvokeURI()
|
||||
}
|
||||
svc := mntxservice.NewService(logger,
|
||||
opts := []mntxservice.Option{
|
||||
mntxservice.WithDiscoveryInvokeURI(invokeURI),
|
||||
mntxservice.WithProducer(producer),
|
||||
mntxservice.WithMonetixConfig(monetixCfg),
|
||||
mntxservice.WithGatewayDescriptor(gatewayDescriptor),
|
||||
mntxservice.WithHTTPClient(&http.Client{Timeout: monetixCfg.Timeout()}),
|
||||
mntxservice.WithStorage(repo),
|
||||
)
|
||||
}
|
||||
if cfg.Messaging != nil {
|
||||
opts = append(opts, mntxservice.WithMessagingSettings(cfg.Messaging.Settings))
|
||||
}
|
||||
svc := mntxservice.NewService(logger, opts...)
|
||||
i.service = svc
|
||||
|
||||
if err := i.startHTTPCallbackServer(svc, callbackCfg); err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
"github.com/tech/sendico/gateway/mntx/storage"
|
||||
"github.com/tech/sendico/gateway/mntx/storage/model"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -30,6 +32,8 @@ type cardPayoutProcessor struct {
|
||||
store storage.Repository
|
||||
httpClient *http.Client
|
||||
producer msg.Producer
|
||||
msgCfg pmodel.SettingsT
|
||||
outbox *gatewayoutbox.ReliableRuntime
|
||||
|
||||
perTxMinAmountMinor int64
|
||||
perTxMinAmountMinorByCurrency map[string]int64
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/tech/sendico/gateway/mntx/storage"
|
||||
"github.com/tech/sendico/pkg/clock"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
)
|
||||
|
||||
@@ -67,3 +68,12 @@ func WithDiscoveryInvokeURI(invokeURI string) Option {
|
||||
s.invokeURI = strings.TrimSpace(invokeURI)
|
||||
}
|
||||
}
|
||||
|
||||
// WithMessagingSettings applies messaging driver settings.
|
||||
func WithMessagingSettings(settings pmodel.SettingsT) Option {
|
||||
return func(s *Service) {
|
||||
if settings != nil {
|
||||
s.msgCfg = settings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
api/gateway/mntx/internal/service/gateway/outbox_reliable.go
Normal file
50
api/gateway/mntx/internal/service/gateway/outbox_reliable.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||
)
|
||||
|
||||
type mntxOutboxProvider interface {
|
||||
Outbox() gatewayoutbox.Store
|
||||
}
|
||||
|
||||
type mntxTransactionProvider interface {
|
||||
TransactionFactory() transaction.Factory
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) outboxStore() gatewayoutbox.Store {
|
||||
provider, ok := p.store.(mntxOutboxProvider)
|
||||
if !ok || provider == nil {
|
||||
return nil
|
||||
}
|
||||
return provider.Outbox()
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) startOutboxReliableProducer() error {
|
||||
if p == nil || p.outbox == nil {
|
||||
return nil
|
||||
}
|
||||
return p.outbox.Start(p.logger, p.producer, p.outboxStore(), p.msgCfg)
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) sendWithOutbox(ctx context.Context, env me.Envelope) error {
|
||||
if err := p.startOutboxReliableProducer(); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.outbox == nil {
|
||||
return nil
|
||||
}
|
||||
return p.outbox.Send(ctx, env)
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) executeTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
provider, ok := p.store.(mntxTransactionProvider)
|
||||
if !ok || provider == nil || provider.TransactionFactory() == nil {
|
||||
return cb(ctx)
|
||||
}
|
||||
return provider.TransactionFactory().CreateTransaction().Execute(ctx, cb)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||
"github.com/tech/sendico/gateway/mntx/internal/appversion"
|
||||
"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
|
||||
"github.com/tech/sendico/gateway/mntx/storage"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
msg "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||
@@ -25,10 +27,12 @@ type Service struct {
|
||||
logger mlogger.Logger
|
||||
clock clockpkg.Clock
|
||||
producer msg.Producer
|
||||
msgCfg pmodel.SettingsT
|
||||
storage storage.Repository
|
||||
config monetix.Config
|
||||
httpClient *http.Client
|
||||
card *cardPayoutProcessor
|
||||
outbox gatewayoutbox.ReliableRuntime
|
||||
gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor
|
||||
announcer *discovery.Announcer
|
||||
invokeURI string
|
||||
@@ -64,6 +68,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
logger: logger.Named("service"),
|
||||
clock: clockpkg.NewSystem(),
|
||||
config: monetix.DefaultConfig(),
|
||||
msgCfg: map[string]any{},
|
||||
}
|
||||
|
||||
initMetrics()
|
||||
@@ -85,6 +90,11 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
}
|
||||
|
||||
svc.card = newCardPayoutProcessor(svc.logger, svc.config, svc.clock, svc.storage, svc.httpClient, svc.producer)
|
||||
svc.card.outbox = &svc.outbox
|
||||
svc.card.msgCfg = svc.msgCfg
|
||||
if err := svc.card.startOutboxReliableProducer(); err != nil {
|
||||
svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err))
|
||||
}
|
||||
svc.card.applyGatewayDescriptor(svc.gatewayDescriptor)
|
||||
svc.startDiscoveryAnnouncer()
|
||||
|
||||
@@ -102,6 +112,7 @@ func (s *Service) Shutdown() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.outbox.Stop()
|
||||
if s.announcer != nil {
|
||||
s.announcer.Stop()
|
||||
}
|
||||
|
||||
@@ -38,27 +38,49 @@ func toOpStatus(t *model.CardPayout) (rail.OperationResult, error) {
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *model.CardPayout) error {
|
||||
if err := p.store.Payouts().Upsert(ctx, state); err != nil {
|
||||
if !isFinalStatus(state) {
|
||||
if err := p.store.Payouts().Upsert(ctx, state); err != nil {
|
||||
p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID),
|
||||
zap.String("payment_ref", state.PaymentRef), zap.String("status", string(state.Status)),
|
||||
)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := p.executeTransaction(ctx, func(txCtx context.Context) (any, error) {
|
||||
if upsertErr := p.store.Payouts().Upsert(txCtx, state); upsertErr != nil {
|
||||
return nil, upsertErr
|
||||
}
|
||||
if isFinalStatus(state) {
|
||||
if emitErr := p.emitTransferStatusEvent(txCtx, state); emitErr != nil {
|
||||
return nil, emitErr
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID),
|
||||
zap.String("payment_ref", state.PaymentRef), zap.String("status", string(state.Status)),
|
||||
)
|
||||
}
|
||||
if isFinalStatus(state) {
|
||||
p.emitTransferStatusEvent(state)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout) {
|
||||
if p == nil || p.producer == nil || payout == nil {
|
||||
return
|
||||
func (p *cardPayoutProcessor) emitTransferStatusEvent(ctx context.Context, payout *model.CardPayout) error {
|
||||
if p == nil || payout == nil {
|
||||
return nil
|
||||
}
|
||||
if p.producer == nil || p.outboxStore() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
status, err := toOpStatus(payout)
|
||||
if err != nil {
|
||||
p.logger.Warn("Failed to convert payout status to operation status for transfer status event", zap.Error(err),
|
||||
mzap.ObjRef("payout_ref", payout.ID), zap.String("payment_ref", payout.PaymentRef), zap.String("status", string(payout.Status)))
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
exec := pmodel.PaymentGatewayExecution{
|
||||
@@ -75,7 +97,9 @@ func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout)
|
||||
TransferRef: payout.GetID().Hex(),
|
||||
}
|
||||
env := paymentgateway.PaymentGatewayExecution(mservice.MntxGateway, &exec)
|
||||
if err := p.producer.SendMessage(env); err != nil {
|
||||
if err := p.sendWithOutbox(ctx, env); err != nil {
|
||||
p.logger.Warn("Failed to publish transfer status event", zap.Error(err), mzap.ObjRef("transfer_ref", payout.ID))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||
"github.com/tech/sendico/gateway/mntx/storage"
|
||||
"github.com/tech/sendico/gateway/mntx/storage/mongo/store"
|
||||
"github.com/tech/sendico/pkg/db"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
@@ -14,11 +16,13 @@ import (
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
logger mlogger.Logger
|
||||
conn *db.MongoConnection
|
||||
db *mongo.Database
|
||||
logger mlogger.Logger
|
||||
conn *db.MongoConnection
|
||||
db *mongo.Database
|
||||
txFactory transaction.Factory
|
||||
|
||||
payouts storage.PayoutsStore
|
||||
outbox gatewayoutbox.Store
|
||||
}
|
||||
|
||||
func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
||||
@@ -42,9 +46,10 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
||||
logger = logger.With(zap.String("database", dbName))
|
||||
}
|
||||
result := &Repository{
|
||||
logger: logger,
|
||||
conn: conn,
|
||||
db: db,
|
||||
logger: logger,
|
||||
conn: conn,
|
||||
db: db,
|
||||
txFactory: newMongoTransactionFactory(client),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -57,7 +62,13 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
||||
result.logger.Error("Failed to initialise payouts store", zap.Error(err), zap.String("store", "payments"))
|
||||
return nil, err
|
||||
}
|
||||
outboxStore, err := gatewayoutbox.NewMongoStore(result.logger, result.db)
|
||||
if err != nil {
|
||||
result.logger.Error("Failed to initialise outbox store", zap.Error(err), zap.String("store", "outbox"))
|
||||
return nil, err
|
||||
}
|
||||
result.payouts = payoutsStore
|
||||
result.outbox = outboxStore
|
||||
result.logger.Info("Payouts gateway MongoDB storage initialised")
|
||||
return result, nil
|
||||
}
|
||||
@@ -66,4 +77,12 @@ func (r *Repository) Payouts() storage.PayoutsStore {
|
||||
return r.payouts
|
||||
}
|
||||
|
||||
func (r *Repository) Outbox() gatewayoutbox.Store {
|
||||
return r.outbox
|
||||
}
|
||||
|
||||
func (r *Repository) TransactionFactory() transaction.Factory {
|
||||
return r.txFactory
|
||||
}
|
||||
|
||||
var _ storage.Repository = (*Repository)(nil)
|
||||
|
||||
38
api/gateway/mntx/storage/mongo/transaction.go
Normal file
38
api/gateway/mntx/storage/mongo/transaction.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type mongoTransactionFactory struct {
|
||||
client *mongo.Client
|
||||
}
|
||||
|
||||
func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction {
|
||||
return &mongoTransaction{client: f.client}
|
||||
}
|
||||
|
||||
type mongoTransaction struct {
|
||||
client *mongo.Client
|
||||
}
|
||||
|
||||
func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
session, err := t.client.StartSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer session.EndSession(ctx)
|
||||
|
||||
run := func(sessCtx context.Context) (any, error) {
|
||||
return cb(sessCtx)
|
||||
}
|
||||
|
||||
return session.WithTransaction(ctx, run)
|
||||
}
|
||||
|
||||
func newMongoTransactionFactory(client *mongo.Client) transaction.Factory {
|
||||
return &mongoTransactionFactory{client: client}
|
||||
}
|
||||
Reference in New Issue
Block a user