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

@@ -6,8 +6,8 @@ replace github.com/tech/sendico/pkg => ../../pkg
require ( require (
github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2 v1.41.1
github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/config v1.32.8
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.8
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
github.com/jung-kurt/gofpdf v1.16.2 github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
@@ -32,7 +32,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
github.com/aws/smithy-go v1.24.0 // indirect github.com/aws/smithy-go v1.24.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@@ -65,6 +65,6 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -8,10 +8,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6ce
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= github.com/aws/aws-sdk-go-v2/config v1.32.8 h1:iu+64gwDKEoKnyTQskSku72dAwggKI5sV6rNvgSMpMs=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= github.com/aws/aws-sdk-go-v2/config v1.32.8/go.mod h1:MI2XvA+qDi3i9AJxX1E2fu730syEBzp/jnXrjxuHwgI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= github.com/aws/aws-sdk-go-v2/credentials v1.19.8 h1:Jp2JYH1lRT3KhX4mshHPvVYsR5qqRec3hGvEarNYoR0=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= github.com/aws/aws-sdk-go-v2/credentials v1.19.8/go.mod h1:fZG9tuvyVfxknv1rKibIz3DobRaFw1Poe8IKtXB3XYY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
@@ -36,8 +36,8 @@ github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 h1:0jbJeuEHlwKJ9PfXtpSFc4MF+WIWORdhN1n30ITZGFM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
@@ -258,8 +258,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -111,7 +111,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
} }
if svc.template == nil { if svc.template == nil {
if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil { if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil {
svc.logger.Warn("failed to load acceptance template", zap.Error(err)) svc.logger.Warn("Failed to load acceptance template", zap.Error(err))
} else { } else {
svc.template = tmpl svc.template = tmpl
} }

View File

@@ -20,7 +20,7 @@ type templateRenderer struct {
func newTemplateRenderer(path string) (*templateRenderer, error) { func newTemplateRenderer(path string) (*templateRenderer, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("read template: %w", err) return nil, fmt.Errorf("Read template: %w", err)
} }
funcs := template.FuncMap{ funcs := template.FuncMap{
@@ -30,7 +30,7 @@ func newTemplateRenderer(path string) (*templateRenderer, error) {
tpl, err := template.New("acceptance").Funcs(funcs).Option("missingkey=error").Parse(string(data)) tpl, err := template.New("acceptance").Funcs(funcs).Option("missingkey=error").Parse(string(data))
if err != nil { if err != nil {
return nil, fmt.Errorf("parse template: %w", err) return nil, fmt.Errorf("Parse template: %w", err)
} }
return &templateRenderer{tpl: tpl}, nil return &templateRenderer{tpl: tpl}, nil
@@ -39,7 +39,7 @@ func newTemplateRenderer(path string) (*templateRenderer, error) {
func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) { func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := r.tpl.Execute(&buf, snapshot); err != nil { if err := r.tpl.Execute(&buf, snapshot); err != nil {
return nil, fmt.Errorf("execute template: %w", err) return nil, fmt.Errorf("Execute template: %w", err)
} }
return renderer.ParseBlocks(buf.String()) return renderer.ParseBlocks(buf.String())
} }

View File

@@ -79,7 +79,7 @@ func ParseBlocks(input string) ([]Block, error) {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("parse blocks: %w", err) return nil, fmt.Errorf("Parse blocks: %w", err)
} }
flush() flush()

View File

@@ -42,13 +42,13 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
defer cancel() defer cancel()
if err := result.Ping(ctx); err != nil { if err := result.Ping(ctx); err != nil {
result.logger.Error("mongo ping failed during store init", zap.Error(err)) result.logger.Error("Mongo ping failed during store init", zap.Error(err))
return nil, err return nil, err
} }
documentsStore, err := store.NewDocuments(result.logger, database) documentsStore, err := store.NewDocuments(result.logger, database)
if err != nil { if err != nil {
result.logger.Error("failed to initialise documents store", zap.Error(err)) result.logger.Error("Failed to initialise documents store", zap.Error(err))
return nil, err return nil, err
} }
result.documents = documentsStore result.documents = documentsStore

View File

@@ -38,13 +38,13 @@ func NewDocuments(logger mlogger.Logger, db *mongo.Database) (*Documents, error)
for _, def := range indexes { for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil { if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection())) logger.Error("Failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err return nil, err
} }
} }
childLogger := logger.Named("documents") childLogger := logger.Named("documents")
childLogger.Debug("documents store initialised") childLogger.Debug("Documents store initialised")
return &Documents{ return &Documents{
logger: childLogger, logger: childLogger,
@@ -68,7 +68,7 @@ func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) er
} }
return err return err
} }
d.logger.Debug("document record created", zap.String("payment_ref", record.PaymentRef)) d.logger.Debug("Document record created", zap.String("payment_ref", record.PaymentRef))
return nil return nil
} }
@@ -124,7 +124,7 @@ func (d *Documents) ListByPaymentRefs(ctx context.Context, paymentRefs []string)
decoder := func(cur *mongo.Cursor) error { decoder := func(cur *mongo.Cursor) error {
var rec model.DocumentRecord var rec model.DocumentRecord
if err := cur.Decode(&rec); err != nil { if err := cur.Decode(&rec); err != nil {
d.logger.Warn("failed to decode document record", zap.Error(err)) d.logger.Warn("Failed to decode document record", zap.Error(err))
return err return err
} }
records = append(records, &rec) records = append(records, &rec)

View File

@@ -50,6 +50,6 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
) )

View File

@@ -208,8 +208,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -116,11 +116,11 @@ func (i *Imp) Start() error {
Insecure: cfg.Oracle.InsecureTransport, Insecure: cfg.Oracle.InsecureTransport,
}) })
if err != nil { if err != nil {
i.logger.Warn("failed to initialise oracle client", zap.String("address", addr), zap.Error(err)) i.logger.Warn("Failed to initialise oracle client", zap.String("address", addr), zap.Error(err))
} else { } else {
oracleClient = oc oracleClient = oc
i.oracleClient = oc i.oracleClient = oc
i.logger.Info("connected to oracle service", zap.String("address", addr)) i.logger.Info("Connected to oracle service", zap.String("address", addr))
} }
} }

View File

@@ -94,7 +94,7 @@ func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, inte
amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule) amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule)
if calcErr != nil { if calcErr != nil {
if !errors.Is(calcErr, merrors.ErrInvalidArg) { if !errors.Is(calcErr, merrors.ErrInvalidArg) {
c.logger.Warn("failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr)) c.logger.Warn("Failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr))
} }
continue continue
} }
@@ -247,7 +247,7 @@ func (c *quoteCalculator) buildFxUsed(ctx context.Context, intent *feesv1.Intent
Provider: provider, Provider: provider,
}) })
if err != nil { if err != nil {
c.logger.Warn("fees: failed to fetch FX context", zap.Error(err)) c.logger.Warn("Fees: failed to fetch FX context", zap.Error(err))
return nil return nil
} }
if snapshot == nil { if snapshot == nil {

View File

@@ -261,7 +261,7 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
var token string var token string
if token, err = encodeTokenPayload(payload); err != nil { if token, err = encodeTokenPayload(payload); err != nil {
logger.Warn("failed to encode fee quote token", zap.Error(err)) logger.Warn("Failed to encode fee quote token", zap.Error(err))
err = status.Error(codes.Internal, "failed to encode fee quote token") err = status.Error(codes.Internal, "failed to encode fee quote token")
return nil, err return nil, err
} }
@@ -333,7 +333,7 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken()) payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken())
if decodeErr != nil { if decodeErr != nil {
resultReason = "invalid_token" resultReason = "invalid_token"
logger.Warn("failed to decode fee quote token", zap.Error(decodeErr)) logger.Warn("Failed to decode fee quote token", zap.Error(decodeErr))
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
return resp, nil return resp, nil
} }
@@ -346,7 +346,7 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
if now.UnixMilli() > payload.ExpiresAtUnixMs { if now.UnixMilli() > payload.ExpiresAtUnixMs {
resultReason = "expired" resultReason = "expired"
logger.Info("fee quote token expired") logger.Info("Fee quote token expired")
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"}
return resp, nil return resp, nil
} }
@@ -354,7 +354,7 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
orgRef, parseErr := bson.ObjectIDFromHex(payload.OrganizationRef) orgRef, parseErr := bson.ObjectIDFromHex(payload.OrganizationRef)
if parseErr != nil { if parseErr != nil {
resultReason = "invalid_token" resultReason = "invalid_token"
logger.Warn("token contained invalid organization reference", zap.Error(parseErr)) logger.Warn("Token contained invalid organization reference", zap.Error(parseErr))
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
return resp, nil return resp, nil
} }
@@ -461,7 +461,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
if errors.Is(calcErr, merrors.ErrInvalidArg) { if errors.Is(calcErr, merrors.ErrInvalidArg) {
return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error()) return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error())
} }
logger.Warn("failed to compute fee quote", zap.Error(calcErr)) logger.Warn("Failed to compute fee quote", zap.Error(calcErr))
return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote") return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote")
} }

View File

@@ -43,13 +43,13 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
defer cancel() defer cancel()
if err := result.Ping(ctx); err != nil { if err := result.Ping(ctx); err != nil {
result.logger.Error("mongo ping failed during store init", zap.Error(err)) result.logger.Error("Mongo ping failed during store init", zap.Error(err))
return nil, err return nil, err
} }
plansStore, err := store.NewPlans(result.logger, database) plansStore, err := store.NewPlans(result.logger, database)
if err != nil { if err != nil {
result.logger.Error("failed to initialise plans store", zap.Error(err)) result.logger.Error("Failed to initialise plans store", zap.Error(err))
return nil, err return nil, err
} }
result.plans = plansStore result.plans = plansStore

View File

@@ -40,7 +40,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
}, },
} }
if err := repo.CreateIndex(orgIndex); err != nil { if err := repo.CreateIndex(orgIndex); err != nil {
logger.Error("failed to ensure fee plan organization index", zap.Error(err)) logger.Error("Failed to ensure fee plan organization index", zap.Error(err))
return nil, err return nil, err
} }
@@ -53,7 +53,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
Unique: true, Unique: true,
} }
if err := repo.CreateIndex(uniqueIndex); err != nil { if err := repo.CreateIndex(uniqueIndex); err != nil {
logger.Error("failed to ensure fee plan uniqueness index", zap.Error(err)) logger.Error("Failed to ensure fee plan uniqueness index", zap.Error(err))
return nil, err return nil, err
} }
@@ -67,7 +67,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
}, },
} }
if err := repo.CreateIndex(activeIndex); err != nil { if err := repo.CreateIndex(activeIndex); err != nil {
logger.Warn("failed to ensure fee plan active index", zap.Error(err)) logger.Warn("Failed to ensure fee plan active index", zap.Error(err))
} }
return &plansStore{ return &plansStore{
@@ -88,7 +88,7 @@ func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error {
if errors.Is(err, merrors.ErrDataConflict) { if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateFeePlan return storage.ErrDuplicateFeePlan
} }
p.logger.Warn("failed to create fee plan", zap.Error(err)) p.logger.Warn("Failed to create fee plan", zap.Error(err))
return err return err
} }
return nil return nil
@@ -106,7 +106,7 @@ func (p *plansStore) Update(ctx context.Context, plan *model.FeePlan) error {
} }
if err := p.repo.Update(ctx, plan); err != nil { if err := p.repo.Update(ctx, plan); err != nil {
p.logger.Warn("failed to update fee plan", zap.Error(err)) p.logger.Warn("Failed to update fee plan", zap.Error(err))
return err return err
} }
return nil return nil

View File

@@ -43,7 +43,7 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -208,8 +208,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -19,10 +19,10 @@ market:
quote: "EUR" quote: "EUR"
symbol: "EURUSDT" symbol: "EURUSDT"
invert: true invert: true
- base: "USD" - base: "USDT"
quote: "USDT" quote: "USD"
symbol: "USDTUSD" symbol: "USDTUSD"
invert: true invert: false
- base: "UAH" - base: "UAH"
quote: "USDT" quote: "USDT"
symbol: "USDTUAH" symbol: "USDTUAH"

View File

@@ -47,7 +47,7 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -208,8 +208,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -49,11 +49,6 @@ const (
defaultRequestTimeout = 10 * time.Second defaultRequestTimeout = 10 * time.Second
) )
const (
maxSymbolParts = 2
isoCodeLen = 3
)
func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:cyclop,funlen,nestif,ireturn func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { //nolint:cyclop,funlen,nestif,ireturn
baseURL := defaultCBRBaseURL baseURL := defaultCBRBaseURL
provider := strings.ToLower(mmodel.DriverCBR.String()) provider := strings.ToLower(mmodel.DriverCBR.String())

View File

@@ -48,5 +48,5 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
) )

View File

@@ -208,8 +208,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -4,14 +4,17 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/ethereum/go-ethereum v1.16.8 github.com/ethereum/go-ethereum v1.17.0
github.com/hashicorp/vault/api v1.22.0 github.com/hashicorp/vault/api v1.22.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tech/sendico/gateway/common v0.1.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
@@ -22,7 +25,7 @@ require (
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
@@ -33,13 +36,13 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/gnark-crypto v0.19.2 // indirect github.com/consensys/gnark-crypto v0.19.2 // indirect
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
@@ -75,6 +78,10 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
@@ -84,5 +91,5 @@ require (
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.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
) )

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 h1:QD30TjDPWtvXb5PBZGZ6Wdvaq7HQixIBtZ/yuseNXc8= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -52,8 +52,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
@@ -78,10 +76,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9igY7law= github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -94,6 +90,7 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -124,6 +121,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -179,8 +180,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -211,8 +210,6 @@ github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -241,10 +238,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -298,16 +293,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -360,8 +355,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -162,6 +162,9 @@ func (i *Imp) Start() error {
gatewayservice.WithDriverRegistry(driverRegistry), gatewayservice.WithDriverRegistry(driverRegistry),
gatewayservice.WithSettings(cfg.Settings), gatewayservice.WithSettings(cfg.Settings),
} }
if cfg.Messaging != nil {
opts = append(opts, gatewayservice.WithMessagingSettings(cfg.Messaging.Settings))
}
svc := gatewayservice.NewService(logger, repo, producer, opts...) svc := gatewayservice.NewService(logger, repo, producer, opts...)
i.service = svc i.service = svc
return svc, nil return svc, nil

View File

@@ -91,3 +91,12 @@ func WithDiscoveryInvokeURI(invokeURI string) Option {
s.invokeURI = strings.TrimSpace(invokeURI) 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
}
}
}

View File

@@ -0,0 +1,47 @@
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 chainOutboxProvider interface {
Outbox() gatewayoutbox.Store
}
type chainTransactionProvider interface {
TransactionFactory() transaction.Factory
}
func (s *Service) outboxStore() gatewayoutbox.Store {
provider, ok := s.storage.(chainOutboxProvider)
if !ok || provider == nil {
return nil
}
return provider.Outbox()
}
func (s *Service) startOutboxReliableProducer() error {
if s == nil || s.storage == nil {
return nil
}
return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg)
}
func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error {
if err := s.startOutboxReliableProducer(); err != nil {
return err
}
return s.outbox.Send(ctx, env)
}
func (s *Service) executeTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
provider, ok := s.storage.(chainTransactionProvider)
if !ok || provider == nil || provider.TransactionFactory() == nil {
return cb(ctx)
}
return provider.TransactionFactory().CreateTransaction().Execute(ctx, cb)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient" "github.com/tech/sendico/gateway/chain/internal/service/gateway/rpcclient"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared" "github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
"github.com/tech/sendico/gateway/chain/storage" "github.com/tech/sendico/gateway/chain/storage"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/api/routers"
"github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/api/routers/gsresponse"
clockpkg "github.com/tech/sendico/pkg/clock" clockpkg "github.com/tech/sendico/pkg/clock"
@@ -22,6 +23,7 @@ import (
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@@ -40,9 +42,11 @@ type Service struct {
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
producer msg.Producer producer msg.Producer
msgCfg pmodel.SettingsT
clock clockpkg.Clock clock clockpkg.Clock
settings CacheSettings settings CacheSettings
outbox gatewayoutbox.ReliableRuntime
networks map[pmodel.ChainNetwork]shared.Network networks map[pmodel.ChainNetwork]shared.Network
serviceWallet shared.ServiceWallet serviceWallet shared.ServiceWallet
@@ -63,6 +67,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
logger: logger.Named("service"), logger: logger.Named("service"),
storage: repo, storage: repo,
producer: producer, producer: producer,
msgCfg: map[string]any{},
clock: clockpkg.System{}, clock: clockpkg.System{},
settings: defaultSettings(), settings: defaultSettings(),
networks: map[pmodel.ChainNetwork]shared.Network{}, networks: map[pmodel.ChainNetwork]shared.Network{},
@@ -84,6 +89,9 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
} }
svc.settings = svc.settings.withDefaults() svc.settings = svc.settings.withDefaults()
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients) svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
if err := svc.startOutboxReliableProducer(); err != nil {
svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err))
}
svc.commands = commands.NewRegistry(commands.RegistryDeps{ svc.commands = commands.NewRegistry(commands.RegistryDeps{
Wallet: commandsWalletDeps(svc), Wallet: commandsWalletDeps(svc),
@@ -105,6 +113,7 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
s.outbox.Stop()
for _, announcer := range s.announcers { for _, announcer := range s.announcers {
if announcer != nil { if announcer != nil {
announcer.Stop() announcer.Stop()

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/tech/sendico/gateway/chain/storage/model" "github.com/tech/sendico/gateway/chain/storage/model"
"github.com/tech/sendico/pkg/merrors"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway" paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
pmodel "github.com/tech/sendico/pkg/model" pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
@@ -13,6 +14,9 @@ import (
) )
func isFinalStatus(t *model.Transfer) bool { func isFinalStatus(t *model.Transfer) bool {
if t == nil {
return false
}
switch t.Status { switch t.Status {
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled: case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
return true return true
@@ -21,16 +25,25 @@ func isFinalStatus(t *model.Transfer) bool {
} }
} }
func toOpStatus(t *model.Transfer) rail.OperationResult { func isFinalTransferStatus(status model.TransferStatus) bool {
switch status {
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
return true
default:
return false
}
}
func toOpStatus(t *model.Transfer) (rail.OperationResult, error) {
switch t.Status { switch t.Status {
case model.TransferStatusFailed: case model.TransferStatusFailed:
return rail.OperationResultFailed return rail.OperationResultFailed, nil
case model.TransferStatusSuccess: case model.TransferStatusSuccess:
return rail.OperationResultSuccess return rail.OperationResultSuccess, nil
case model.TransferStatusCancelled: case model.TransferStatusCancelled:
return rail.OperationResultCancelled return rail.OperationResultCancelled, nil
default: default:
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status)) return rail.OperationResultFailed, merrors.InvalidArgument(fmt.Sprintf("unexpected transfer status: %s", t.Status), "transfer.status")
} }
} }
@@ -45,19 +58,47 @@ func toError(t *model.Transfer) string {
} }
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) { func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash) if !isFinalTransferStatus(status) {
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
}
return transfer, err
}
res, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) {
transfer, statusErr := s.storage.Transfers().UpdateStatus(txCtx, transferRef, status, failureReason, txHash)
if statusErr != nil {
return nil, statusErr
}
if isFinalStatus(transfer) {
if emitErr := s.emitTransferStatusEvent(txCtx, transfer); emitErr != nil {
return nil, emitErr
}
}
return transfer, nil
})
if err != nil { if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err)) s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
return nil, err
} }
if isFinalStatus(transfer) {
s.emitTransferStatusEvent(transfer) transfer, _ := res.(*model.Transfer)
} return transfer, nil
return transfer, err
} }
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) { func (s *Service) emitTransferStatusEvent(ctx context.Context, transfer *model.Transfer) error {
if s == nil || s.producer == nil || transfer == nil { if s == nil || transfer == nil {
return return nil
}
if s.producer == nil || s.outboxStore() == nil {
return nil
}
status, err := toOpStatus(transfer)
if err != nil {
s.logger.Warn("Failed to map transfer status for transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
return err
} }
exec := pmodel.PaymentGatewayExecution{ exec := pmodel.PaymentGatewayExecution{
@@ -65,13 +106,15 @@ func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
IdempotencyKey: transfer.IdempotencyKey, IdempotencyKey: transfer.IdempotencyKey,
ExecutedMoney: transfer.NetAmount, ExecutedMoney: transfer.NetAmount,
PaymentRef: transfer.PaymentRef, PaymentRef: transfer.PaymentRef,
Status: toOpStatus(transfer), Status: status,
OperationRef: transfer.OperationRef, OperationRef: transfer.OperationRef,
Error: toError(transfer), Error: toError(transfer),
TransferRef: transfer.TransferRef, TransferRef: transfer.TransferRef,
} }
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec) env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
if err := s.producer.SendMessage(env); err != nil { if err := s.sendWithOutbox(ctx, env); err != nil {
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef)) s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
return err
} }
return nil
} }

View File

@@ -6,7 +6,9 @@ import (
"github.com/tech/sendico/gateway/chain/storage" "github.com/tech/sendico/gateway/chain/storage"
"github.com/tech/sendico/gateway/chain/storage/mongo/store" "github.com/tech/sendico/gateway/chain/storage/mongo/store"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo"
@@ -15,13 +17,15 @@ import (
// Store implements storage.Repository backed by MongoDB. // Store implements storage.Repository backed by MongoDB.
type Store struct { type Store struct {
logger mlogger.Logger logger mlogger.Logger
conn *db.MongoConnection conn *db.MongoConnection
db *mongo.Database db *mongo.Database
txFactory transaction.Factory
wallets storage.WalletsStore wallets storage.WalletsStore
transfers storage.TransfersStore transfers storage.TransfersStore
deposits storage.DepositsStore deposits storage.DepositsStore
outbox gatewayoutbox.Store
} }
// New creates a new Mongo-backed repository. // New creates a new Mongo-backed repository.
@@ -35,9 +39,10 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
} }
result := &Store{ result := &Store{
logger: logger.Named("storage").Named("mongo"), logger: logger.Named("storage").Named("mongo"),
conn: conn, conn: conn,
db: conn.Database(), db: conn.Database(),
txFactory: newMongoTransactionFactory(client),
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -63,10 +68,16 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
result.logger.Error("Failed to initialise deposits store", zap.Error(err)) result.logger.Error("Failed to initialise deposits store", zap.Error(err))
return nil, err 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))
return nil, err
}
result.wallets = walletsStore result.wallets = walletsStore
result.transfers = transfersStore result.transfers = transfersStore
result.deposits = depositsStore result.deposits = depositsStore
result.outbox = outboxStore
result.logger.Info("Chain gateway MongoDB storage initialised") result.logger.Info("Chain gateway MongoDB storage initialised")
return result, nil return result, nil
@@ -95,4 +106,12 @@ func (s *Store) Deposits() storage.DepositsStore {
return s.deposits return s.deposits
} }
func (s *Store) Outbox() gatewayoutbox.Store {
return s.outbox
}
func (s *Store) TransactionFactory() transaction.Factory {
return s.txFactory
}
var _ storage.Repository = (*Store)(nil) var _ storage.Repository = (*Store)(nil)

View 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}
}

30
api/gateway/common/go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/tech/sendico/gateway/common
go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg
require (
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
)
require (
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
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/protobuf v1.36.11 // indirect
)

158
api/gateway/common/go.sum Normal file
View File

@@ -0,0 +1,158 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY=
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,33 @@
package outbox
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
)
const Collection = "outbox"
type Status string
const (
StatusPending Status = "pending"
StatusSent Status = "sent"
StatusFailed Status = "failed"
)
// Event represents an outbox message pending dispatch to the broker.
type Event struct {
storable.Base `bson:",inline" json:",inline"`
EventID string `bson:"eventId" json:"eventId"`
Subject string `bson:"subject" json:"subject"`
Payload []byte `bson:"payload" json:"payload"`
Status Status `bson:"status" json:"status"`
Attempts int `bson:"attempts" json:"attempts"`
SentAt *time.Time `bson:"sentAt,omitempty" json:"sentAt,omitempty"`
}
func (*Event) Collection() string {
return Collection
}

View File

@@ -0,0 +1,123 @@
package outbox
import (
"context"
"time"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.uber.org/zap"
)
type mongoStore struct {
logger mlogger.Logger
repo repository.Repository
}
func NewMongoStore(logger mlogger.Logger, db *mongo.Database) (Store, error) {
if db == nil {
return nil, merrors.InvalidArgument("mongo database is nil")
}
if logger == nil {
logger = zap.NewNop()
}
repo := repository.CreateMongoRepository(db, Collection)
statusIndex := &ri.Definition{
Keys: []ri.Key{{Field: "status", Sort: ri.Asc}, {Field: "createdAt", Sort: ri.Asc}},
}
if err := repo.CreateIndex(statusIndex); err != nil {
logger.Error("Failed to ensure outbox status index", zap.Error(err))
return nil, err
}
eventIDIndex := &ri.Definition{
Keys: []ri.Key{{Field: "eventId", Sort: ri.Asc}},
Unique: true,
}
if err := repo.CreateIndex(eventIDIndex); err != nil {
logger.Error("Failed to ensure outbox eventId index", zap.Error(err))
return nil, err
}
childLogger := logger.Named(Collection)
childLogger.Debug("Outbox store initialised", zap.String("collection", Collection))
return &mongoStore{logger: childLogger, repo: repo}, nil
}
func (o *mongoStore) Create(ctx context.Context, event *Event) error {
if event == nil {
o.logger.Warn("Attempt to create nil outbox event")
return merrors.InvalidArgument("outbox: nil event")
}
if err := o.repo.Insert(ctx, event, nil); err != nil {
if mongo.IsDuplicateKeyError(err) {
o.logger.Warn("Duplicate outbox event id", zap.String("event_id", event.EventID))
return merrors.DataConflict("outbox event with this id already exists")
}
o.logger.Warn("Failed to create outbox event", zap.Error(err))
return err
}
o.logger.Debug("Outbox event created", zap.String("event_id", event.EventID), zap.String("subject", event.Subject))
return nil
}
func (o *mongoStore) ListPending(ctx context.Context, limit int) ([]*Event, error) {
limit64 := int64(limit)
query := repository.Query().
Filter(repository.Field("status"), StatusPending).
Limit(&limit64).
Sort(repository.Field("createdAt"), true)
events := make([]*Event, 0)
err := o.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
doc := &Event{}
if err := cur.Decode(doc); err != nil {
return err
}
events = append(events, doc)
return nil
})
if err != nil {
o.logger.Warn("Failed to list pending outbox events", zap.Error(err))
return nil, err
}
return events, nil
}
func (o *mongoStore) MarkSent(ctx context.Context, eventRef bson.ObjectID, sentAt time.Time) error {
if eventRef.IsZero() {
return merrors.InvalidArgument("outbox: zero event id")
}
patch := repository.Patch().
Set(repository.Field("status"), StatusSent).
Set(repository.Field("sentAt"), sentAt)
return o.repo.Patch(ctx, eventRef, patch)
}
func (o *mongoStore) MarkFailed(ctx context.Context, eventRef bson.ObjectID) error {
if eventRef.IsZero() {
return merrors.InvalidArgument("outbox: zero event id")
}
patch := repository.Patch().Set(repository.Field("status"), StatusFailed)
return o.repo.Patch(ctx, eventRef, patch)
}
func (o *mongoStore) IncrementAttempts(ctx context.Context, eventRef bson.ObjectID) error {
if eventRef.IsZero() {
return merrors.InvalidArgument("outbox: zero event id")
}
patch := repository.Patch().Inc(repository.Field("attempts"), 1)
return o.repo.Patch(ctx, eventRef, patch)
}

View File

@@ -0,0 +1,108 @@
package outbox
import (
"context"
"strings"
"time"
pmessaging "github.com/tech/sendico/pkg/messaging"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger"
cfgmodel "github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
type reliableStoreAdapter struct {
store Store
}
func NewReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store Store, messagingSettings cfgmodel.SettingsT, opts ...pmessagingreliable.Option) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) {
if store == nil {
return nil, pmessagingreliable.DefaultSettings(), nil
}
producer, settings, err := pmessagingreliable.NewReliableProducerFromConfig(logger, direct, &reliableStoreAdapter{store: store}, messagingSettings, opts...)
if err != nil {
return nil, pmessagingreliable.Settings{}, err
}
return producer, settings, nil
}
func (a *reliableStoreAdapter) Enqueue(ctx context.Context, msg pmessagingreliable.OutboxMessage) error {
if a == nil || a.store == nil {
return nil
}
return a.store.Create(ctx, &Event{
EventID: strings.TrimSpace(msg.EventID),
Subject: strings.TrimSpace(msg.Subject),
Payload: append([]byte(nil), msg.Payload...),
Status: StatusPending,
Attempts: msg.Attempts,
})
}
func (a *reliableStoreAdapter) ListPending(ctx context.Context, limit int) ([]pmessagingreliable.OutboxMessage, error) {
if a == nil || a.store == nil {
return nil, nil
}
events, err := a.store.ListPending(ctx, limit)
if err != nil {
return nil, err
}
result := make([]pmessagingreliable.OutboxMessage, 0, len(events))
for _, event := range events {
if event == nil {
continue
}
reference := ""
if eventRef := event.GetID(); eventRef != nil && !eventRef.IsZero() {
reference = eventRef.Hex()
}
result = append(result, pmessagingreliable.OutboxMessage{
Reference: reference,
EventID: strings.TrimSpace(event.EventID),
Subject: strings.TrimSpace(event.Subject),
Payload: append([]byte(nil), event.Payload...),
Attempts: event.Attempts,
CreatedAt: event.CreatedAt,
})
}
return result, nil
}
func (a *reliableStoreAdapter) MarkSent(ctx context.Context, reference string, sentAt time.Time) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.MarkSent(ctx, eventRef, sentAt)
}
func (a *reliableStoreAdapter) MarkFailed(ctx context.Context, reference string) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.MarkFailed(ctx, eventRef)
}
func (a *reliableStoreAdapter) IncrementAttempts(ctx context.Context, reference string) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.IncrementAttempts(ctx, eventRef)
}
func parseObjectID(raw string) (bson.ObjectID, error) {
return bson.ObjectIDFromHex(raw)
}

View File

@@ -0,0 +1,330 @@
package outbox
import (
"context"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
domainmodel "github.com/tech/sendico/pkg/model"
notification "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func TestGatewayReliableProducerPersistsAndRetriesOnBrokerFailure(t *testing.T) {
store := newMemoryOutboxStore()
broker := &flakyDirectProducer{failuresRemaining: 1}
producer, _, err := NewReliableProducer(
zap.NewNop(),
broker,
store,
nil,
pmessagingreliable.WithBatchSize(1),
pmessagingreliable.WithMaxAttempts(3),
)
if err != nil {
t.Fatalf("failed to create reliable producer: %v", err)
}
env := newTestEnvelope(t, []byte(`{"transferRef":"tx-1","status":"pending"}`))
if err := producer.SendWithOutbox(context.Background(), env); err != nil {
t.Fatalf("failed to enqueue envelope into outbox: %v", err)
}
eventID := env.GetMessageId().String()
persisted := store.EventByID(eventID)
if persisted == nil {
t.Fatalf("expected outbox event %s to be persisted", eventID)
}
if persisted.Status != StatusPending {
t.Fatalf("expected pending status after enqueue, got %q", persisted.Status)
}
if persisted.Attempts != 0 {
t.Fatalf("expected zero attempts after enqueue, got %d", persisted.Attempts)
}
processed, err := producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("first dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected first dispatch to process 1 event, got %d", processed)
}
afterFailure := store.EventByID(eventID)
if afterFailure == nil {
t.Fatalf("expected outbox event %s to exist after broker failure", eventID)
}
if afterFailure.Status != StatusPending {
t.Fatalf("expected event to stay pending after transient broker error, got %q", afterFailure.Status)
}
if afterFailure.Attempts != 1 {
t.Fatalf("expected attempts to increment to 1 after failure, got %d", afterFailure.Attempts)
}
if afterFailure.SentAt != nil {
t.Fatalf("expected sentAt to be empty after failed publish")
}
processed, err = producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("second dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected second dispatch to process 1 event, got %d", processed)
}
afterRetry := store.EventByID(eventID)
if afterRetry == nil {
t.Fatalf("expected outbox event %s to exist after retry", eventID)
}
if afterRetry.Status != StatusSent {
t.Fatalf("expected event to be sent after retry, got %q", afterRetry.Status)
}
if afterRetry.Attempts != 1 {
t.Fatalf("expected attempts to remain 1 after successful retry, got %d", afterRetry.Attempts)
}
if afterRetry.SentAt == nil {
t.Fatalf("expected sentAt to be set after successful publish")
}
if attempts := broker.Attempts(); attempts != 2 {
t.Fatalf("expected two broker attempts (fail then success), got %d", attempts)
}
}
func TestGatewayReliableProducerMarksFailedAfterMaxAttempts(t *testing.T) {
store := newMemoryOutboxStore()
broker := &flakyDirectProducer{failuresRemaining: 10}
producer, _, err := NewReliableProducer(
zap.NewNop(),
broker,
store,
nil,
pmessagingreliable.WithBatchSize(1),
pmessagingreliable.WithMaxAttempts(2),
)
if err != nil {
t.Fatalf("failed to create reliable producer: %v", err)
}
env := newTestEnvelope(t, []byte(`{"transferRef":"tx-2","status":"pending"}`))
if err := producer.SendWithOutbox(context.Background(), env); err != nil {
t.Fatalf("failed to enqueue envelope into outbox: %v", err)
}
eventID := env.GetMessageId().String()
processed, err := producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("first dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected first dispatch to process 1 event, got %d", processed)
}
processed, err = producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("second dispatch failed: %v", err)
}
if processed != 1 {
t.Fatalf("expected second dispatch to process 1 event, got %d", processed)
}
processed, err = producer.DispatchPending(context.Background())
if err != nil {
t.Fatalf("third dispatch failed: %v", err)
}
if processed != 0 {
t.Fatalf("expected failed event to be excluded from pending queue, got processed=%d", processed)
}
final := store.EventByID(eventID)
if final == nil {
t.Fatalf("expected outbox event %s to exist", eventID)
}
if final.Status != StatusFailed {
t.Fatalf("expected event to be marked failed after max attempts, got %q", final.Status)
}
if final.Attempts != 2 {
t.Fatalf("expected attempts to equal max attempts (2), got %d", final.Attempts)
}
if final.SentAt != nil {
t.Fatalf("expected sentAt to remain empty for failed event")
}
}
func newTestEnvelope(t *testing.T, payload []byte) me.Envelope {
t.Helper()
env := me.CreateEnvelope("gateway.common.outbox.test", domainmodel.NewNotification(mservice.ChainGateway, notification.NAUpdated))
if _, err := env.Wrap(payload); err != nil {
t.Fatalf("failed to wrap test payload: %v", err)
}
return env
}
type memoryOutboxStore struct {
mu sync.Mutex
eventsByRef map[bson.ObjectID]*Event
refByEvent map[string]bson.ObjectID
}
func newMemoryOutboxStore() *memoryOutboxStore {
return &memoryOutboxStore{
eventsByRef: make(map[bson.ObjectID]*Event),
refByEvent: make(map[string]bson.ObjectID),
}
}
func (s *memoryOutboxStore) Create(_ context.Context, event *Event) error {
if event == nil {
return errors.New("event is nil")
}
s.mu.Lock()
defer s.mu.Unlock()
eventID := strings.TrimSpace(event.EventID)
if eventID == "" {
return errors.New("event id is required")
}
if _, exists := s.refByEvent[eventID]; exists {
return errors.New("duplicate event id")
}
stored := cloneEvent(event)
stored.SetID(bson.NewObjectID())
if stored.Status == "" {
stored.Status = StatusPending
}
ref := *stored.GetID()
s.eventsByRef[ref] = stored
s.refByEvent[eventID] = ref
return nil
}
func (s *memoryOutboxStore) ListPending(_ context.Context, limit int) ([]*Event, error) {
s.mu.Lock()
defer s.mu.Unlock()
pending := make([]*Event, 0, len(s.eventsByRef))
for _, event := range s.eventsByRef {
if event.Status == StatusPending {
pending = append(pending, cloneEvent(event))
}
}
sort.Slice(pending, func(i, j int) bool {
return pending[i].CreatedAt.Before(pending[j].CreatedAt)
})
if limit > 0 && len(pending) > limit {
pending = pending[:limit]
}
return pending, nil
}
func (s *memoryOutboxStore) MarkSent(_ context.Context, eventRef bson.ObjectID, sentAt time.Time) error {
s.mu.Lock()
defer s.mu.Unlock()
event, ok := s.eventsByRef[eventRef]
if !ok {
return errors.New("event not found")
}
event.Status = StatusSent
when := sentAt.UTC()
event.SentAt = &when
event.Update()
return nil
}
func (s *memoryOutboxStore) MarkFailed(_ context.Context, eventRef bson.ObjectID) error {
s.mu.Lock()
defer s.mu.Unlock()
event, ok := s.eventsByRef[eventRef]
if !ok {
return errors.New("event not found")
}
event.Status = StatusFailed
event.Update()
return nil
}
func (s *memoryOutboxStore) IncrementAttempts(_ context.Context, eventRef bson.ObjectID) error {
s.mu.Lock()
defer s.mu.Unlock()
event, ok := s.eventsByRef[eventRef]
if !ok {
return errors.New("event not found")
}
event.Attempts++
event.Update()
return nil
}
func (s *memoryOutboxStore) EventByID(eventID string) *Event {
s.mu.Lock()
defer s.mu.Unlock()
ref, ok := s.refByEvent[eventID]
if !ok {
return nil
}
event, ok := s.eventsByRef[ref]
if !ok {
return nil
}
return cloneEvent(event)
}
func cloneEvent(event *Event) *Event {
if event == nil {
return nil
}
copyEvent := *event
copyEvent.Payload = append([]byte(nil), event.Payload...)
if event.SentAt != nil {
sentAt := *event.SentAt
copyEvent.SentAt = &sentAt
}
return &copyEvent
}
type flakyDirectProducer struct {
mu sync.Mutex
failuresRemaining int
attempts int
}
func (p *flakyDirectProducer) SendMessage(_ me.Envelope) error {
p.mu.Lock()
defer p.mu.Unlock()
p.attempts++
if p.failuresRemaining > 0 {
p.failuresRemaining--
return errors.New("broker unavailable")
}
return nil
}
func (p *flakyDirectProducer) Attempts() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.attempts
}

View File

@@ -0,0 +1,72 @@
package outbox
import (
"context"
"sync"
"github.com/tech/sendico/pkg/merrors"
pmessaging "github.com/tech/sendico/pkg/messaging"
me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger"
cfgmodel "github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
// ReliableRuntime owns a reliable producer lifecycle for gateway outbox dispatch.
type ReliableRuntime struct {
once sync.Once
cancel context.CancelFunc
producer *pmessagingreliable.ReliableProducer
settings pmessagingreliable.Settings
initErr error
}
func (r *ReliableRuntime) Start(logger mlogger.Logger, direct pmessaging.Producer, store Store, messagingSettings cfgmodel.SettingsT, opts ...pmessagingreliable.Option) error {
if r == nil {
return nil
}
if logger == nil {
logger = zap.NewNop()
}
logger = logger.Named("outbox_reliable")
r.once.Do(func() {
reliableProducer, settings, err := NewReliableProducer(logger, direct, store, messagingSettings, opts...)
if err != nil {
r.initErr = err
return
}
r.producer = reliableProducer
r.settings = settings
if r.producer == nil || direct == nil {
logger.Info("Outbox reliable publisher disabled", zap.Bool("enabled", settings.Enabled))
return
}
logger.Info("Outbox reliable publisher configured",
zap.Bool("enabled", settings.Enabled),
zap.Int("batch_size", settings.BatchSize),
zap.Int("poll_interval_seconds", settings.PollIntervalSeconds),
zap.Int("max_attempts", settings.MaxAttempts))
ctx, cancel := context.WithCancel(context.Background())
r.cancel = cancel
go r.producer.Run(ctx)
})
return r.initErr
}
func (r *ReliableRuntime) Send(ctx context.Context, envelope me.Envelope) error {
if r == nil || r.producer == nil {
return merrors.Internal("reliable outbox producer is not configured")
}
return r.producer.SendWithOutbox(ctx, envelope)
}
func (r *ReliableRuntime) Stop() {
if r == nil || r.cancel == nil {
return
}
r.cancel()
}

View File

@@ -0,0 +1,17 @@
package outbox
import (
"context"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
)
// Store persists gateway outbox events.
type Store interface {
Create(ctx context.Context, event *Event) error
ListPending(ctx context.Context, limit int) ([]*Event, error)
MarkSent(ctx context.Context, eventRef bson.ObjectID, sentAt time.Time) error
MarkFailed(ctx context.Context, eventRef bson.ObjectID) error
IncrementAttempts(ctx context.Context, eventRef bson.ObjectID) error
}

View File

@@ -4,10 +4,13 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require ( require (
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/gateway/common v0.1.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
@@ -48,5 +51,5 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
) )

View File

@@ -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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -191,14 +191,18 @@ func (i *Imp) Start() error {
if cfg.GRPC != nil { if cfg.GRPC != nil {
invokeURI = cfg.GRPC.DiscoveryInvokeURI() invokeURI = cfg.GRPC.DiscoveryInvokeURI()
} }
svc := mntxservice.NewService(logger, opts := []mntxservice.Option{
mntxservice.WithDiscoveryInvokeURI(invokeURI), mntxservice.WithDiscoveryInvokeURI(invokeURI),
mntxservice.WithProducer(producer), mntxservice.WithProducer(producer),
mntxservice.WithMonetixConfig(monetixCfg), mntxservice.WithMonetixConfig(monetixCfg),
mntxservice.WithGatewayDescriptor(gatewayDescriptor), mntxservice.WithGatewayDescriptor(gatewayDescriptor),
mntxservice.WithHTTPClient(&http.Client{Timeout: monetixCfg.Timeout()}), mntxservice.WithHTTPClient(&http.Client{Timeout: monetixCfg.Timeout()}),
mntxservice.WithStorage(repo), mntxservice.WithStorage(repo),
) }
if cfg.Messaging != nil {
opts = append(opts, mntxservice.WithMessagingSettings(cfg.Messaging.Settings))
}
svc := mntxservice.NewService(logger, opts...)
i.service = svc i.service = svc
if err := i.startHTTPCallbackServer(svc, callbackCfg); err != nil { if err := i.startHTTPCallbackServer(svc, callbackCfg); err != nil {

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"github.com/shopspring/decimal" "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/internal/service/monetix"
"github.com/tech/sendico/gateway/mntx/storage" "github.com/tech/sendico/gateway/mntx/storage"
"github.com/tech/sendico/gateway/mntx/storage/model" "github.com/tech/sendico/gateway/mntx/storage/model"
@@ -17,6 +18,7 @@ import (
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1" gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1" mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@@ -30,6 +32,8 @@ type cardPayoutProcessor struct {
store storage.Repository store storage.Repository
httpClient *http.Client httpClient *http.Client
producer msg.Producer producer msg.Producer
msgCfg pmodel.SettingsT
outbox *gatewayoutbox.ReliableRuntime
perTxMinAmountMinor int64 perTxMinAmountMinor int64
perTxMinAmountMinorByCurrency map[string]int64 perTxMinAmountMinorByCurrency map[string]int64

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/gateway/mntx/storage" "github.com/tech/sendico/gateway/mntx/storage"
"github.com/tech/sendico/pkg/clock" "github.com/tech/sendico/pkg/clock"
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
pmodel "github.com/tech/sendico/pkg/model"
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1" gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
) )
@@ -67,3 +68,12 @@ func WithDiscoveryInvokeURI(invokeURI string) Option {
s.invokeURI = strings.TrimSpace(invokeURI) 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
}
}
}

View 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)
}

View File

@@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strings" "strings"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/gateway/mntx/internal/appversion" "github.com/tech/sendico/gateway/mntx/internal/appversion"
"github.com/tech/sendico/gateway/mntx/internal/service/monetix" "github.com/tech/sendico/gateway/mntx/internal/service/monetix"
"github.com/tech/sendico/gateway/mntx/storage" "github.com/tech/sendico/gateway/mntx/storage"
@@ -14,6 +15,7 @@ import (
"github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/discovery"
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1" gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
@@ -25,10 +27,12 @@ type Service struct {
logger mlogger.Logger logger mlogger.Logger
clock clockpkg.Clock clock clockpkg.Clock
producer msg.Producer producer msg.Producer
msgCfg pmodel.SettingsT
storage storage.Repository storage storage.Repository
config monetix.Config config monetix.Config
httpClient *http.Client httpClient *http.Client
card *cardPayoutProcessor card *cardPayoutProcessor
outbox gatewayoutbox.ReliableRuntime
gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor
announcer *discovery.Announcer announcer *discovery.Announcer
invokeURI string invokeURI string
@@ -64,6 +68,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
logger: logger.Named("service"), logger: logger.Named("service"),
clock: clockpkg.NewSystem(), clock: clockpkg.NewSystem(),
config: monetix.DefaultConfig(), config: monetix.DefaultConfig(),
msgCfg: map[string]any{},
} }
initMetrics() 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 = 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.card.applyGatewayDescriptor(svc.gatewayDescriptor)
svc.startDiscoveryAnnouncer() svc.startDiscoveryAnnouncer()
@@ -102,6 +112,7 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
s.outbox.Stop()
if s.announcer != nil { if s.announcer != nil {
s.announcer.Stop() s.announcer.Stop()
} }

View File

@@ -38,27 +38,49 @@ func toOpStatus(t *model.CardPayout) (rail.OperationResult, error) {
} }
func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *model.CardPayout) 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), 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)), zap.String("payment_ref", state.PaymentRef), zap.String("status", string(state.Status)),
) )
} return err
if isFinalStatus(state) {
p.emitTransferStatusEvent(state)
} }
return nil return nil
} }
func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout) { func (p *cardPayoutProcessor) emitTransferStatusEvent(ctx context.Context, payout *model.CardPayout) error {
if p == nil || p.producer == nil || payout == nil { if p == nil || payout == nil {
return return nil
}
if p.producer == nil || p.outboxStore() == nil {
return nil
} }
status, err := toOpStatus(payout) status, err := toOpStatus(payout)
if err != nil { if err != nil {
p.logger.Warn("Failed to convert payout status to operation status for transfer status event", zap.Error(err), 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))) mzap.ObjRef("payout_ref", payout.ID), zap.String("payment_ref", payout.PaymentRef), zap.String("status", string(payout.Status)))
return return err
} }
exec := pmodel.PaymentGatewayExecution{ exec := pmodel.PaymentGatewayExecution{
@@ -75,7 +97,9 @@ func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout)
TransferRef: payout.GetID().Hex(), TransferRef: payout.GetID().Hex(),
} }
env := paymentgateway.PaymentGatewayExecution(mservice.MntxGateway, &exec) 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)) p.logger.Warn("Failed to publish transfer status event", zap.Error(err), mzap.ObjRef("transfer_ref", payout.ID))
return err
} }
return nil
} }

View File

@@ -4,9 +4,11 @@ import (
"context" "context"
"time" "time"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/gateway/mntx/storage" "github.com/tech/sendico/gateway/mntx/storage"
"github.com/tech/sendico/gateway/mntx/storage/mongo/store" "github.com/tech/sendico/gateway/mntx/storage/mongo/store"
"github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo"
@@ -14,11 +16,13 @@ import (
) )
type Repository struct { type Repository struct {
logger mlogger.Logger logger mlogger.Logger
conn *db.MongoConnection conn *db.MongoConnection
db *mongo.Database db *mongo.Database
txFactory transaction.Factory
payouts storage.PayoutsStore payouts storage.PayoutsStore
outbox gatewayoutbox.Store
} }
func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) { 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)) logger = logger.With(zap.String("database", dbName))
} }
result := &Repository{ result := &Repository{
logger: logger, logger: logger,
conn: conn, conn: conn,
db: db, db: db,
txFactory: newMongoTransactionFactory(client),
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() 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")) result.logger.Error("Failed to initialise payouts store", zap.Error(err), zap.String("store", "payments"))
return nil, err 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.payouts = payoutsStore
result.outbox = outboxStore
result.logger.Info("Payouts gateway MongoDB storage initialised") result.logger.Info("Payouts gateway MongoDB storage initialised")
return result, nil return result, nil
} }
@@ -66,4 +77,12 @@ func (r *Repository) Payouts() storage.PayoutsStore {
return r.payouts 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) var _ storage.Repository = (*Repository)(nil)

View 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}
}

View File

@@ -4,7 +4,10 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require ( require (
github.com/tech/sendico/gateway/common v0.1.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
@@ -45,5 +48,5 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
) )

View File

@@ -208,8 +208,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -90,13 +90,18 @@ func (i *Imp) Start() error {
if cfg.GRPC != nil { if cfg.GRPC != nil {
invokeURI = cfg.GRPC.DiscoveryInvokeURI() invokeURI = cfg.GRPC.DiscoveryInvokeURI()
} }
msgSettings := map[string]any(nil)
if cfg.Messaging != nil {
msgSettings = cfg.Messaging.Settings
}
gwCfg := gateway.Config{ gwCfg := gateway.Config{
Rail: cfg.Gateway.Rail, Rail: cfg.Gateway.Rail,
TargetChatIDEnv: cfg.Gateway.TargetChatIDEnv, TargetChatIDEnv: cfg.Gateway.TargetChatIDEnv,
TimeoutSeconds: cfg.Gateway.TimeoutSeconds, TimeoutSeconds: cfg.Gateway.TimeoutSeconds,
AcceptedUserIDs: cfg.Gateway.AcceptedUserIDs, AcceptedUserIDs: cfg.Gateway.AcceptedUserIDs,
SuccessReaction: cfg.Gateway.SuccessReaction, SuccessReaction: cfg.Gateway.SuccessReaction,
InvokeURI: invokeURI, InvokeURI: invokeURI,
MessagingSettings: msgSettings,
} }
svc := gateway.NewService(logger, repo, producer, broker, gwCfg) svc := gateway.NewService(logger, repo, producer, broker, gwCfg)
i.service = svc i.service = svc

View File

@@ -0,0 +1,47 @@
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 tgOutboxProvider interface {
Outbox() gatewayoutbox.Store
}
type tgTransactionProvider interface {
TransactionFactory() transaction.Factory
}
func (s *Service) outboxStore() gatewayoutbox.Store {
provider, ok := s.repo.(tgOutboxProvider)
if !ok || provider == nil {
return nil
}
return provider.Outbox()
}
func (s *Service) startOutboxReliableProducer() error {
if s == nil || s.repo == nil {
return nil
}
return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg)
}
func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error {
if err := s.startOutboxReliableProducer(); err != nil {
return err
}
return s.outbox.Send(ctx, env)
}
func (s *Service) executeTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
provider, ok := s.repo.(tgTransactionProvider)
if !ok || provider == nil || provider.TransactionFactory() == nil {
return cb(ctx)
}
return provider.TransactionFactory().CreateTransaction().Execute(ctx, cb)
}

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/gateway/tgsettle/storage" "github.com/tech/sendico/gateway/tgsettle/storage"
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model" storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
"github.com/tech/sendico/pkg/api/routers" "github.com/tech/sendico/pkg/api/routers"
@@ -20,6 +21,7 @@ import (
tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram" tnotifications "github.com/tech/sendico/pkg/messaging/notifications/telegram"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
paymenttypes "github.com/tech/sendico/pkg/payments/types" paymenttypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
@@ -48,12 +50,13 @@ const (
) )
type Config struct { type Config struct {
Rail string Rail string
TargetChatIDEnv string TargetChatIDEnv string
TimeoutSeconds int32 TimeoutSeconds int32
AcceptedUserIDs []string AcceptedUserIDs []string
SuccessReaction string SuccessReaction string
InvokeURI string InvokeURI string
MessagingSettings pmodel.SettingsT
} }
type Service struct { type Service struct {
@@ -62,11 +65,13 @@ type Service struct {
producer msg.Producer producer msg.Producer
broker mb.Broker broker mb.Broker
cfg Config cfg Config
msgCfg pmodel.SettingsT
rail string rail string
chatID string chatID string
announcer *discovery.Announcer announcer *discovery.Announcer
invokeURI string invokeURI string
successReaction string successReaction string
outbox gatewayoutbox.ReliableRuntime
consumers []msg.Consumer consumers []msg.Consumer
@@ -84,6 +89,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
producer: producer, producer: producer,
broker: broker, broker: broker,
cfg: cfg, cfg: cfg,
msgCfg: cfg.MessagingSettings,
rail: strings.TrimSpace(cfg.Rail), rail: strings.TrimSpace(cfg.Rail),
invokeURI: strings.TrimSpace(cfg.InvokeURI), invokeURI: strings.TrimSpace(cfg.InvokeURI),
} }
@@ -92,6 +98,9 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
if svc.successReaction == "" { if svc.successReaction == "" {
svc.successReaction = defaultTelegramSuccessReaction svc.successReaction = defaultTelegramSuccessReaction
} }
if err := svc.startOutboxReliableProducer(); err != nil {
svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err))
}
svc.startConsumers() svc.startConsumers()
svc.startAnnouncer() svc.startAnnouncer()
return svc return svc
@@ -107,6 +116,7 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
s.outbox.Stop()
if s.announcer != nil { if s.announcer != nil {
s.announcer.Stop() s.announcer.Stop()
} }

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/tech/sendico/gateway/tgsettle/storage/model" "github.com/tech/sendico/gateway/tgsettle/storage/model"
"github.com/tech/sendico/pkg/merrors"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway" paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
pmodel "github.com/tech/sendico/pkg/model" pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
@@ -21,33 +22,57 @@ func isFinalStatus(t *model.PaymentRecord) bool {
} }
} }
func toOpStatus(t *model.PaymentRecord) rail.OperationResult { func toOpStatus(t *model.PaymentRecord) (rail.OperationResult, error) {
switch t.Status { switch t.Status {
case model.PaymentStatusFailed: case model.PaymentStatusFailed:
return rail.OperationResultFailed return rail.OperationResultFailed, nil
case model.PaymentStatusSuccess: case model.PaymentStatusSuccess:
return rail.OperationResultSuccess return rail.OperationResultSuccess, nil
case model.PaymentStatusCancelled: case model.PaymentStatusCancelled:
return rail.OperationResultCancelled return rail.OperationResultCancelled, nil
default: default:
panic("unexpected transfer status") return rail.OperationResultFailed, merrors.InvalidArgument("unexpected transfer status", "payment.status")
} }
} }
func (s *Service) updateTransferStatus(ctx context.Context, record *model.PaymentRecord) error { func (s *Service) updateTransferStatus(ctx context.Context, record *model.PaymentRecord) error {
if err := s.repo.Payments().Upsert(ctx, record); err != nil { if !isFinalStatus(record) {
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("payment_ref", record.PaymentIntentID), zap.String("status", string(record.Status)), zap.Error(err))
return err
}
return nil
}
_, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) {
if upsertErr := s.repo.Payments().Upsert(txCtx, record); upsertErr != nil {
return nil, upsertErr
}
if isFinalStatus(record) {
if emitErr := s.emitTransferStatusEvent(txCtx, record); emitErr != nil {
return nil, emitErr
}
}
return nil, nil
})
if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("payment_ref", record.PaymentIntentID), zap.String("status", string(record.Status)), zap.Error(err)) s.logger.Warn("Failed to update transfer status", zap.String("payment_ref", record.PaymentIntentID), zap.String("status", string(record.Status)), zap.Error(err))
return err return err
} }
if isFinalStatus(record) {
s.emitTransferStatusEvent(ctx, record)
}
return nil return nil
} }
func (s *Service) emitTransferStatusEvent(_ context.Context, record *model.PaymentRecord) { func (s *Service) emitTransferStatusEvent(ctx context.Context, record *model.PaymentRecord) error {
if s == nil || s.producer == nil || record == nil { if s == nil || record == nil {
return return nil
}
if s.producer == nil || s.outboxStore() == nil {
return nil
}
status, err := toOpStatus(record)
if err != nil {
s.logger.Warn("Failed to map transfer status for transfer status event", zap.Error(err), mzap.ObjRef("transfer_ref", record.ID))
return err
} }
exec := pmodel.PaymentGatewayExecution{ exec := pmodel.PaymentGatewayExecution{
@@ -55,13 +80,15 @@ func (s *Service) emitTransferStatusEvent(_ context.Context, record *model.Payme
IdempotencyKey: record.IdempotencyKey, IdempotencyKey: record.IdempotencyKey,
ExecutedMoney: record.ExecutedMoney, ExecutedMoney: record.ExecutedMoney,
PaymentRef: record.PaymentRef, PaymentRef: record.PaymentRef,
Status: toOpStatus(record), Status: status,
OperationRef: record.OperationRef, OperationRef: record.OperationRef,
Error: record.FailureReason, Error: record.FailureReason,
TransferRef: record.ID.Hex(), TransferRef: record.ID.Hex(),
} }
env := paymentgateway.PaymentGatewayExecution(mservice.MntxGateway, &exec) env := paymentgateway.PaymentGatewayExecution(mservice.MntxGateway, &exec)
if err := s.producer.SendMessage(env); err != nil { if sendErr := s.sendWithOutbox(ctx, env); sendErr != nil {
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), mzap.ObjRef("transfer_ref", record.ID)) s.logger.Warn("Failed to publish transfer status event", zap.Error(sendErr), mzap.ObjRef("transfer_ref", record.ID))
return sendErr
} }
return nil
} }

View File

@@ -4,9 +4,11 @@ import (
"context" "context"
"time" "time"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/gateway/tgsettle/storage" "github.com/tech/sendico/gateway/tgsettle/storage"
"github.com/tech/sendico/gateway/tgsettle/storage/mongo/store" "github.com/tech/sendico/gateway/tgsettle/storage/mongo/store"
"github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo"
@@ -14,12 +16,14 @@ import (
) )
type Repository struct { type Repository struct {
logger mlogger.Logger logger mlogger.Logger
conn *db.MongoConnection conn *db.MongoConnection
db *mongo.Database db *mongo.Database
txFactory transaction.Factory
payments storage.PaymentsStore payments storage.PaymentsStore
tg storage.TelegramConfirmationsStore tg storage.TelegramConfirmationsStore
outbox gatewayoutbox.Store
} }
func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) { func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
@@ -43,9 +47,10 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
logger = logger.With(zap.String("database", dbName)) logger = logger.With(zap.String("database", dbName))
} }
result := &Repository{ result := &Repository{
logger: logger, logger: logger,
conn: conn, conn: conn,
db: db, db: db,
txFactory: newMongoTransactionFactory(client),
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@@ -63,8 +68,14 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
result.logger.Error("Failed to initialise telegram confirmations store", zap.Error(err), zap.String("store", "telegram_confirmations")) result.logger.Error("Failed to initialise telegram confirmations store", zap.Error(err), zap.String("store", "telegram_confirmations"))
return nil, err 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.payments = paymentsStore result.payments = paymentsStore
result.tg = tgStore result.tg = tgStore
result.outbox = outboxStore
result.logger.Info("Payment gateway MongoDB storage initialised") result.logger.Info("Payment gateway MongoDB storage initialised")
return result, nil return result, nil
} }
@@ -77,4 +88,12 @@ func (r *Repository) TelegramConfirmations() storage.TelegramConfirmationsStore
return r.tg return r.tg
} }
func (r *Repository) Outbox() gatewayoutbox.Store {
return r.outbox
}
func (r *Repository) TransactionFactory() transaction.Factory {
return r.txFactory
}
var _ storage.Repository = (*Repository)(nil) var _ storage.Repository = (*Repository)(nil)

View 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}
}

View File

@@ -4,9 +4,11 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/gateway/common => ../common
require ( require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/ethereum/go-ethereum v1.16.8 github.com/ethereum/go-ethereum v1.17.0
github.com/fbsobreira/gotron-sdk v0.24.1 github.com/fbsobreira/gotron-sdk v0.24.1
github.com/hashicorp/vault/api v1.22.0 github.com/hashicorp/vault/api v1.22.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
@@ -14,6 +16,7 @@ require (
github.com/shengdoushi/base58 v1.0.0 github.com/shengdoushi/base58 v1.0.0
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tech/sendico/gateway/common v0.1.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
@@ -24,7 +27,7 @@ require (
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
@@ -36,14 +39,14 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/gnark-crypto v0.19.2 // indirect github.com/consensys/gnark-crypto v0.19.2 // indirect
github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set v1.8.0 // indirect github.com/deckarep/golang-set v1.8.0 // indirect
github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
@@ -83,6 +86,10 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
@@ -92,6 +99,6 @@ require (
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // 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
) )

View File

@@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 h1:QD30TjDPWtvXb5PBZGZ6Wdvaq7HQixIBtZ/yuseNXc8= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -54,8 +54,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc=
github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
@@ -82,10 +80,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9igY7law= github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes=
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fbsobreira/gotron-sdk v0.24.1 h1:YxvF26zyXNkho1GxywQeq/gRi70aQ6sbWYop6OTWL7E= github.com/fbsobreira/gotron-sdk v0.24.1 h1:YxvF26zyXNkho1GxywQeq/gRi70aQ6sbWYop6OTWL7E=
@@ -100,6 +96,7 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -131,6 +128,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -186,8 +187,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -218,8 +217,6 @@ github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -250,12 +247,10 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -313,16 +308,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -379,10 +374,10 @@ 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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= 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-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= 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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -179,6 +179,9 @@ func (i *Imp) Start() error {
gatewayservice.WithDriverRegistry(driverRegistry), gatewayservice.WithDriverRegistry(driverRegistry),
gatewayservice.WithSettings(cfg.Settings), gatewayservice.WithSettings(cfg.Settings),
} }
if cfg.Messaging != nil {
opts = append(opts, gatewayservice.WithMessagingSettings(cfg.Messaging.Settings))
}
svc := gatewayservice.NewService(logger, repo, producer, opts...) svc := gatewayservice.NewService(logger, repo, producer, opts...)
i.service = svc i.service = svc
return svc, nil return svc, nil

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/gateway/tron/internal/service/gateway/tronclient" "github.com/tech/sendico/gateway/tron/internal/service/gateway/tronclient"
"github.com/tech/sendico/gateway/tron/shared" "github.com/tech/sendico/gateway/tron/shared"
clockpkg "github.com/tech/sendico/pkg/clock" clockpkg "github.com/tech/sendico/pkg/clock"
pmodel "github.com/tech/sendico/pkg/model"
) )
// Option configures the Service. // Option configures the Service.
@@ -98,3 +99,12 @@ func WithDiscoveryInvokeURI(invokeURI string) Option {
s.invokeURI = strings.TrimSpace(invokeURI) 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
}
}
}

View File

@@ -0,0 +1,47 @@
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 tronOutboxProvider interface {
Outbox() gatewayoutbox.Store
}
type tronTransactionProvider interface {
TransactionFactory() transaction.Factory
}
func (s *Service) outboxStore() gatewayoutbox.Store {
provider, ok := s.storage.(tronOutboxProvider)
if !ok || provider == nil {
return nil
}
return provider.Outbox()
}
func (s *Service) startOutboxReliableProducer() error {
if s == nil || s.storage == nil {
return nil
}
return s.outbox.Start(s.logger, s.producer, s.outboxStore(), s.msgCfg)
}
func (s *Service) sendWithOutbox(ctx context.Context, env me.Envelope) error {
if err := s.startOutboxReliableProducer(); err != nil {
return err
}
return s.outbox.Send(ctx, env)
}
func (s *Service) executeTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
provider, ok := s.storage.(tronTransactionProvider)
if !ok || provider == nil || provider.TransactionFactory() == nil {
return cb(ctx)
}
return provider.TransactionFactory().CreateTransaction().Execute(ctx, cb)
}

View File

@@ -3,6 +3,7 @@ package gateway
import ( import (
"context" "context"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/gateway/tron/internal/appversion" "github.com/tech/sendico/gateway/tron/internal/appversion"
"github.com/tech/sendico/gateway/tron/internal/keymanager" "github.com/tech/sendico/gateway/tron/internal/keymanager"
"github.com/tech/sendico/gateway/tron/internal/service/gateway/commands" "github.com/tech/sendico/gateway/tron/internal/service/gateway/commands"
@@ -19,9 +20,11 @@ import (
"github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/discovery"
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
"google.golang.org/grpc" "google.golang.org/grpc"
) )
@@ -40,9 +43,11 @@ type Service struct {
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
producer msg.Producer producer msg.Producer
msgCfg pmodel.SettingsT
clock clockpkg.Clock clock clockpkg.Clock
settings CacheSettings settings CacheSettings
outbox gatewayoutbox.ReliableRuntime
networks map[string]shared.Network networks map[string]shared.Network
serviceWallet shared.ServiceWallet serviceWallet shared.ServiceWallet
@@ -64,6 +69,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
logger: logger.Named("service"), logger: logger.Named("service"),
storage: repo, storage: repo,
producer: producer, producer: producer,
msgCfg: map[string]any{},
clock: clockpkg.System{}, clock: clockpkg.System{},
settings: defaultSettings(), settings: defaultSettings(),
networks: map[string]shared.Network{}, networks: map[string]shared.Network{},
@@ -85,6 +91,9 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
} }
svc.settings = svc.settings.withDefaults() svc.settings = svc.settings.withDefaults()
svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients) svc.networkRegistry = rpcclient.NewRegistry(svc.networks, svc.rpcClients)
if err := svc.startOutboxReliableProducer(); err != nil {
svc.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err))
}
svc.commands = commands.NewRegistry(commands.RegistryDeps{ svc.commands = commands.NewRegistry(commands.RegistryDeps{
Wallet: commandsWalletDeps(svc), Wallet: commandsWalletDeps(svc),
@@ -106,6 +115,7 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
s.outbox.Stop()
for _, announcer := range s.announcers { for _, announcer := range s.announcers {
if announcer != nil { if announcer != nil {
announcer.Stop() announcer.Stop()

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/tech/sendico/gateway/tron/storage/model" "github.com/tech/sendico/gateway/tron/storage/model"
"github.com/tech/sendico/pkg/merrors"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway" paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
pmodel "github.com/tech/sendico/pkg/model" pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
@@ -13,6 +14,9 @@ import (
) )
func isFinalStatus(t *model.Transfer) bool { func isFinalStatus(t *model.Transfer) bool {
if t == nil {
return false
}
switch t.Status { switch t.Status {
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled: case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
return true return true
@@ -21,16 +25,25 @@ func isFinalStatus(t *model.Transfer) bool {
} }
} }
func toOpStatus(t *model.Transfer) rail.OperationResult { func isFinalTransferStatus(status model.TransferStatus) bool {
switch status {
case model.TransferStatusFailed, model.TransferStatusSuccess, model.TransferStatusCancelled:
return true
default:
return false
}
}
func toOpStatus(t *model.Transfer) (rail.OperationResult, error) {
switch t.Status { switch t.Status {
case model.TransferStatusFailed: case model.TransferStatusFailed:
return rail.OperationResultFailed return rail.OperationResultFailed, nil
case model.TransferStatusSuccess: case model.TransferStatusSuccess:
return rail.OperationResultSuccess return rail.OperationResultSuccess, nil
case model.TransferStatusCancelled: case model.TransferStatusCancelled:
return rail.OperationResultCancelled return rail.OperationResultCancelled, nil
default: default:
panic(fmt.Sprintf("toOpStatus: unexpected transfer status: %s", t.Status)) return rail.OperationResultFailed, merrors.InvalidArgument(fmt.Sprintf("unexpected transfer status: %s", t.Status), "transfer.status")
} }
} }
@@ -45,19 +58,47 @@ func toError(t *model.Transfer) string {
} }
func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) { func (s *Service) updateTransferStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason, txHash string) (*model.Transfer, error) {
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash) if !isFinalTransferStatus(status) {
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
}
return transfer, err
}
res, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) {
transfer, statusErr := s.storage.Transfers().UpdateStatus(txCtx, transferRef, status, failureReason, txHash)
if statusErr != nil {
return nil, statusErr
}
if isFinalStatus(transfer) {
if emitErr := s.emitTransferStatusEvent(txCtx, transfer); emitErr != nil {
return nil, emitErr
}
}
return transfer, nil
})
if err != nil { if err != nil {
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err)) s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
return nil, err
} }
if isFinalStatus(transfer) {
s.emitTransferStatusEvent(transfer) transfer, _ := res.(*model.Transfer)
} return transfer, nil
return transfer, err
} }
func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) { func (s *Service) emitTransferStatusEvent(ctx context.Context, transfer *model.Transfer) error {
if s == nil || s.producer == nil || transfer == nil { if s == nil || transfer == nil {
return return nil
}
if s.producer == nil || s.outboxStore() == nil {
return nil
}
status, err := toOpStatus(transfer)
if err != nil {
s.logger.Warn("Failed to map transfer status for transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
return err
} }
exec := pmodel.PaymentGatewayExecution{ exec := pmodel.PaymentGatewayExecution{
@@ -65,13 +106,15 @@ func (s *Service) emitTransferStatusEvent(transfer *model.Transfer) {
IdempotencyKey: transfer.IdempotencyKey, IdempotencyKey: transfer.IdempotencyKey,
ExecutedMoney: transfer.NetAmount, ExecutedMoney: transfer.NetAmount,
PaymentRef: transfer.PaymentRef, PaymentRef: transfer.PaymentRef,
Status: toOpStatus(transfer), Status: status,
OperationRef: transfer.OperationRef, OperationRef: transfer.OperationRef,
Error: toError(transfer), Error: toError(transfer),
TransferRef: transfer.TransferRef, TransferRef: transfer.TransferRef,
} }
env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec) env := paymentgateway.PaymentGatewayExecution(mservice.ChainGateway, &exec)
if err := s.producer.SendMessage(env); err != nil { if err := s.sendWithOutbox(ctx, env); err != nil {
s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef)) s.logger.Warn("Failed to publish transfer status event", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
return err
} }
return nil
} }

View File

@@ -4,9 +4,11 @@ import (
"context" "context"
"time" "time"
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
"github.com/tech/sendico/gateway/tron/storage" "github.com/tech/sendico/gateway/tron/storage"
"github.com/tech/sendico/gateway/tron/storage/mongo/store" "github.com/tech/sendico/gateway/tron/storage/mongo/store"
"github.com/tech/sendico/pkg/db" "github.com/tech/sendico/pkg/db"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo"
@@ -15,13 +17,15 @@ import (
// Store implements storage.Repository backed by MongoDB. // Store implements storage.Repository backed by MongoDB.
type Store struct { type Store struct {
logger mlogger.Logger logger mlogger.Logger
conn *db.MongoConnection conn *db.MongoConnection
db *mongo.Database db *mongo.Database
txFactory transaction.Factory
wallets storage.WalletsStore wallets storage.WalletsStore
transfers storage.TransfersStore transfers storage.TransfersStore
deposits storage.DepositsStore deposits storage.DepositsStore
outbox gatewayoutbox.Store
} }
// New creates a new Mongo-backed repository. // New creates a new Mongo-backed repository.
@@ -35,9 +39,10 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
} }
result := &Store{ result := &Store{
logger: logger.Named("storage").Named("mongo"), logger: logger.Named("storage").Named("mongo"),
conn: conn, conn: conn,
db: conn.Database(), db: conn.Database(),
txFactory: newMongoTransactionFactory(client),
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -63,10 +68,16 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
result.logger.Error("Failed to initialise deposits store", zap.Error(err)) result.logger.Error("Failed to initialise deposits store", zap.Error(err))
return nil, err 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))
return nil, err
}
result.wallets = walletsStore result.wallets = walletsStore
result.transfers = transfersStore result.transfers = transfersStore
result.deposits = depositsStore result.deposits = depositsStore
result.outbox = outboxStore
result.logger.Info("Chain gateway MongoDB storage initialised") result.logger.Info("Chain gateway MongoDB storage initialised")
return result, nil return result, nil
@@ -95,4 +106,12 @@ func (s *Store) Deposits() storage.DepositsStore {
return s.deposits return s.deposits
} }
func (s *Store) Outbox() gatewayoutbox.Store {
return s.outbox
}
func (s *Store) TransactionFactory() transaction.Factory {
return s.txFactory
}
var _ storage.Repository = (*Store)(nil) var _ storage.Repository = (*Store)(nil)

View 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}
}

View File

@@ -34,6 +34,12 @@ messaging:
max_reconnects: 10 max_reconnects: 10
reconnect_wait: 5 reconnect_wait: 5
buffer_size: 1024 buffer_size: 1024
# Optional: remove this block to use package defaults.
reliable_publisher:
enabled: true
batch_size: 100
poll_interval_seconds: 1
max_attempts: 5
fees: fees:
address: "dev-billing-fees:50060" address: "dev-billing-fees:50060"

View File

@@ -34,6 +34,12 @@ messaging:
max_reconnects: 10 max_reconnects: 10
reconnect_wait: 5 reconnect_wait: 5
buffer_size: 1024 buffer_size: 1024
# Optional: remove this block to use package defaults.
reliable_publisher:
enabled: true
batch_size: 100
poll_interval_seconds: 1
max_attempts: 5
fees: fees:
address: "sendico_billing_fees:50060" address: "sendico_billing_fees:50060"

View File

@@ -1,6 +1,6 @@
module github.com/tech/sendico/ledger module github.com/tech/sendico/ledger
go 1.24.0 go 1.25.0
replace github.com/tech/sendico/pkg => ../pkg replace github.com/tech/sendico/pkg => ../pkg
@@ -49,5 +49,5 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
) )

View File

@@ -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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -1,35 +0,0 @@
package ledger
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/mservice"
)
// Delivery status enum
type OutboxStatus string
const (
OutboxPending OutboxStatus = "pending"
OutboxSent OutboxStatus = "sent"
OutboxFailed OutboxStatus = "failed" // terminal after max retries, or keep pending with NextAttemptAt=nil
)
type OutboxEvent struct {
storable.Base `bson:",inline" json:",inline"`
EventID string `bson:"eventId" json:"eventId"` // deterministic; use as NATS Msg-Id
Subject string `bson:"subject" json:"subject"` // NATS subject / stream routing key
Payload []byte `bson:"payload" json:"payload"` // JSON (or other) payload
Status OutboxStatus `bson:"status" json:"status"` // enum
Attempts int `bson:"attempts" json:"attempts"` // total tries
NextAttemptAt *time.Time `bson:"nextAttemptAt,omitempty" json:"nextAttemptAt,omitempty"` // for backoff scheduler
SentAt *time.Time `bson:"sentAt,omitempty" json:"sentAt,omitempty"`
LastError string `bson:"lastError,omitempty" json:"lastError,omitempty"` // brief reason of last failure
CorrelationRef string `bson:"correlationRef,omitempty" json:"correlationRef,omitempty"` // e.g., journalEntryRef or idempotencyKey
}
func (o *OutboxEvent) Collection() string {
return mservice.LedgerOutbox
}

View File

@@ -120,7 +120,14 @@ func (i *Imp) Start() error {
if cfg.GRPC != nil { if cfg.GRPC != nil {
invokeURI = cfg.GRPC.DiscoveryInvokeURI() invokeURI = cfg.GRPC.DiscoveryInvokeURI()
} }
svc := ledger.NewService(logger, repo, producer, feesClient, feesTimeout, invokeURI) msgSettings := map[string]any(nil)
if cfg.Messaging != nil {
msgSettings = cfg.Messaging.Settings
}
svc, err := ledger.NewService(logger, repo, producer, msgSettings, feesClient, feesTimeout, invokeURI)
if err != nil {
return nil, err
}
if err := svc.EnsureSystemAccounts(context.Background()); err != nil { if err := svc.EnsureSystemAccounts(context.Background()); err != nil {
return nil, err return nil, err
} }

View File

@@ -38,7 +38,7 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData("account not found") return nil, merrors.NoData("account not found")
} }
logger.Warn("failed to get account for block", zap.Error(err)) logger.Warn("Failed to get account for block", zap.Error(err))
return nil, merrors.Internal("failed to get account") return nil, merrors.Internal("failed to get account")
} }
@@ -61,17 +61,17 @@ func (s *Service) blockAccountResponder(_ context.Context, req *ledgerv1.BlockAc
} }
if account.Status == pmodel.LedgerAccountStatusFrozen { if account.Status == pmodel.LedgerAccountStatusFrozen {
logger.Debug("account already frozen", mzap.AccRef(accountRef)) logger.Debug("Account already frozen", mzap.AccRef(accountRef))
return &ledgerv1.BlockAccountResponse{Account: toProtoAccount(account)}, nil return &ledgerv1.BlockAccountResponse{Account: toProtoAccount(account)}, nil
} }
if err := s.storage.Accounts().UpdateStatus(ctx, accountRef, pmodel.LedgerAccountStatusFrozen); err != nil { if err := s.storage.Accounts().UpdateStatus(ctx, accountRef, pmodel.LedgerAccountStatusFrozen); err != nil {
logger.Warn("failed to freeze account", zap.Error(err)) logger.Warn("Failed to freeze account", zap.Error(err))
return nil, merrors.Internal("failed to block account") return nil, merrors.Internal("failed to block account")
} }
account.Status = pmodel.LedgerAccountStatusFrozen account.Status = pmodel.LedgerAccountStatusFrozen
logger.Info("account blocked (frozen)", mzap.AccRef(accountRef)) logger.Info("Account blocked (frozen)", mzap.AccRef(accountRef))
return &ledgerv1.BlockAccountResponse{Account: toProtoAccount(account)}, nil return &ledgerv1.BlockAccountResponse{Account: toProtoAccount(account)}, nil
} }
} }
@@ -101,7 +101,7 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData("account not found") return nil, merrors.NoData("account not found")
} }
logger.Warn("failed to get account for unblock", zap.Error(err)) logger.Warn("Failed to get account for unblock", zap.Error(err))
return nil, merrors.Internal("failed to get account") return nil, merrors.Internal("failed to get account")
} }
@@ -124,17 +124,17 @@ func (s *Service) unblockAccountResponder(_ context.Context, req *ledgerv1.Unblo
} }
if account.Status == pmodel.LedgerAccountStatusActive { if account.Status == pmodel.LedgerAccountStatusActive {
logger.Debug("account already active", mzap.AccRef(accountRef)) logger.Debug("Account already active", mzap.AccRef(accountRef))
return &ledgerv1.UnblockAccountResponse{Account: toProtoAccount(account)}, nil return &ledgerv1.UnblockAccountResponse{Account: toProtoAccount(account)}, nil
} }
if err := s.storage.Accounts().UpdateStatus(ctx, accountRef, pmodel.LedgerAccountStatusActive); err != nil { if err := s.storage.Accounts().UpdateStatus(ctx, accountRef, pmodel.LedgerAccountStatusActive); err != nil {
logger.Warn("failed to activate account", zap.Error(err)) logger.Warn("Failed to activate account", zap.Error(err))
return nil, merrors.Internal("failed to unblock account") return nil, merrors.Internal("failed to unblock account")
} }
account.Status = pmodel.LedgerAccountStatusActive account.Status = pmodel.LedgerAccountStatusActive
logger.Info("account unblocked (active)", mzap.AccRef(accountRef)) logger.Info("Account unblocked (active)", mzap.AccRef(accountRef))
return &ledgerv1.UnblockAccountResponse{Account: toProtoAccount(account)}, nil return &ledgerv1.UnblockAccountResponse{Account: toProtoAccount(account)}, nil
} }
} }

View File

@@ -48,7 +48,7 @@ func (s *Service) listAccountsResponder(_ context.Context, req *ledgerv1.ListAcc
// No pagination requested; return all accounts for the organization. // No pagination requested; return all accounts for the organization.
accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, filter, 0, 0) accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, filter, 0, 0)
if err != nil { if err != nil {
s.logger.Warn("failed to list ledger accounts", zap.Error(err), mzap.ObjRef("organization_ref", orgRef)) s.logger.Warn("Failed to list ledger accounts", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return nil, err return nil, err
} }

View File

@@ -1,207 +0,0 @@
package ledger
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/tech/sendico/ledger/storage"
ledgerModel "github.com/tech/sendico/ledger/storage/model"
"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"
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"
)
const (
defaultOutboxBatchSize = 100
defaultOutboxPollInterval = time.Second
maxOutboxDeliveryAttempts = 5
outboxPublisherSender = "ledger.outbox.publisher"
)
type outboxPublisher struct {
logger mlogger.Logger
store storage.OutboxStore
producer pmessaging.Producer
batchSize int
pollInterval time.Duration
}
func newOutboxPublisher(logger mlogger.Logger, store storage.OutboxStore, producer pmessaging.Producer) *outboxPublisher {
return &outboxPublisher{
logger: logger.Named("outbox.publisher"),
store: store,
producer: producer,
batchSize: defaultOutboxBatchSize,
pollInterval: defaultOutboxPollInterval,
}
}
func (p *outboxPublisher) run(ctx context.Context) {
p.logger.Info("started")
defer p.logger.Info("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 ledger outbox events", zap.Error(err))
}
if processed > 0 {
p.logger.Debug("dispatched ledger outbox events",
zap.Int("count", processed),
zap.Int("batch_size", p.batchSize))
}
if ctx.Err() != nil {
return
}
if processed == 0 {
select {
case <-ctx.Done():
return
case <-time.After(p.pollInterval):
}
}
}
}
func (p *outboxPublisher) dispatchPending(ctx context.Context) (int, error) {
if p.store == nil || p.producer == nil {
return 0, nil
}
events, err := p.store.ListPending(ctx, p.batchSize)
if err != nil {
return 0, err
}
for _, event := range events {
if ctx.Err() != nil {
return len(events), ctx.Err()
}
if err := p.publishEvent(ctx, event); err != nil {
if errors.Is(err, context.Canceled) {
return len(events), err
}
p.logger.Warn("failed to publish outbox event",
zap.Error(err),
zap.String("eventId", event.EventID),
zap.String("subject", event.Subject),
zap.String("organizationRef", event.OrganizationRef.Hex()),
zap.Int("attempts", event.Attempts))
p.handleFailure(ctx, event)
continue
}
if err := p.markSent(ctx, event); err != nil {
if errors.Is(err, context.Canceled) {
return len(events), err
}
p.logger.Warn("failed to mark outbox event as sent",
zap.Error(err),
zap.String("eventId", event.EventID),
zap.String("subject", event.Subject),
zap.String("organizationRef", event.OrganizationRef.Hex()))
} else {
p.logger.Debug("outbox event marked sent",
zap.String("eventId", event.EventID),
zap.String("subject", event.Subject),
zap.String("organizationRef", event.OrganizationRef.Hex()))
}
}
return len(events), nil
}
func (p *outboxPublisher) publishEvent(_ context.Context, event *ledgerModel.OutboxEvent) error {
docID := event.GetID()
if docID == nil || docID.IsZero() {
return merrors.InvalidArgument("outbox event missing identifier")
}
payload, err := p.wrapPayload(event)
if err != nil {
return err
}
env := me.CreateEnvelope(outboxPublisherSender, domainmodel.NewNotification(mservice.LedgerOutbox, notification.NASent))
if _, err = env.Wrap(payload); err != nil {
return err
}
return p.producer.SendMessage(env)
}
func (p *outboxPublisher) wrapPayload(event *ledgerModel.OutboxEvent) ([]byte, error) {
message := ledgerOutboxMessage{
EventID: event.EventID,
Subject: event.Subject,
Payload: json.RawMessage(event.Payload),
Attempts: event.Attempts,
OrganizationRef: event.OrganizationRef.Hex(),
CreatedAt: event.CreatedAt,
}
return json.Marshal(message)
}
func (p *outboxPublisher) markSent(ctx context.Context, event *ledgerModel.OutboxEvent) error {
eventRef := event.GetID()
if eventRef == nil || eventRef.IsZero() {
return merrors.InvalidArgument("outbox event missing identifier")
}
return p.store.MarkSent(ctx, *eventRef, time.Now().UTC())
}
func (p *outboxPublisher) handleFailure(ctx context.Context, event *ledgerModel.OutboxEvent) {
eventRef := event.GetID()
if eventRef == nil || eventRef.IsZero() {
p.logger.Warn("cannot record outbox failure: missing identifier", zap.String("eventId", event.EventID))
return
}
if err := p.store.IncrementAttempts(ctx, *eventRef); err != nil && !errors.Is(err, context.Canceled) {
p.logger.Warn("failed to increment outbox attempts",
zap.Error(err),
zap.String("eventId", event.EventID),
zap.String("subject", event.Subject),
zap.String("organizationRef", event.OrganizationRef.Hex()))
}
if event.Attempts+1 >= maxOutboxDeliveryAttempts {
if err := p.store.MarkFailed(ctx, *eventRef); err != nil && !errors.Is(err, context.Canceled) {
p.logger.Warn("failed to mark outbox event failed",
zap.Error(err),
zap.String("eventId", event.EventID),
zap.String("subject", event.Subject),
zap.String("organizationRef", event.OrganizationRef.Hex()),
zap.Int("attempts", event.Attempts+1))
} else {
p.logger.Warn("ledger outbox event marked as failed",
zap.String("eventId", event.EventID),
zap.String("subject", event.Subject),
zap.String("organizationRef", event.OrganizationRef.Hex()),
zap.Int("attempts", event.Attempts+1))
}
}
}
type ledgerOutboxMessage struct {
EventID string `json:"eventId"`
Subject string `json:"subject"`
Payload json.RawMessage `json:"payload"`
Attempts int `json:"attempts"`
OrganizationRef string `json:"organizationRef"`
CreatedAt time.Time `json:"createdAt"`
}

View File

@@ -0,0 +1,168 @@
package ledger
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/tech/sendico/ledger/storage"
"github.com/tech/sendico/ledger/storage/model"
pmessaging "github.com/tech/sendico/pkg/messaging"
me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger"
cfgmodel "github.com/tech/sendico/pkg/model"
domainmodel "github.com/tech/sendico/pkg/model"
notification "github.com/tech/sendico/pkg/model/notification"
"github.com/tech/sendico/pkg/mservice"
)
const (
outboxPublisherSender = "ledger.outbox.publisher"
)
type ledgerOutboxMessage struct {
EventID string `json:"eventId"`
Subject string `json:"subject"`
Payload json.RawMessage `json:"payload"`
Attempts int `json:"attempts"`
OrganizationRef string `json:"organizationRef"`
CreatedAt time.Time `json:"createdAt"`
}
type ledgerOutboxStoreAdapter struct {
store storage.OutboxStore
}
func newLedgerReliableProducer(logger mlogger.Logger, direct pmessaging.Producer, store storage.OutboxStore, messagingSettings cfgmodel.SettingsT) (*pmessagingreliable.ReliableProducer, pmessagingreliable.Settings, error) {
if store == nil {
return nil, pmessagingreliable.DefaultSettings(), nil
}
producer, settings, err := pmessagingreliable.NewReliableProducerFromConfig(logger, direct, &ledgerOutboxStoreAdapter{store: store}, messagingSettings,
pmessagingreliable.WithEnvelopeDecoder(ledgerOutboxDecoder),
)
if err != nil {
return nil, pmessagingreliable.Settings{}, err
}
return producer, settings, nil
}
func (a *ledgerOutboxStoreAdapter) Enqueue(ctx context.Context, msg pmessagingreliable.OutboxMessage) error {
if a == nil || a.store == nil {
return nil
}
event := &model.OutboxEvent{
EventID: strings.TrimSpace(msg.EventID),
Subject: strings.TrimSpace(msg.Subject),
Payload: append([]byte(nil), msg.Payload...),
Status: model.OutboxStatusPending,
Attempts: msg.Attempts,
}
if organizationRef := strings.TrimSpace(msg.OrganizationRef); organizationRef != "" {
orgRef, err := parseObjectID(organizationRef)
if err != nil {
return err
}
event.OrganizationRef = orgRef
}
return a.store.Create(ctx, event)
}
func (a *ledgerOutboxStoreAdapter) ListPending(ctx context.Context, limit int) ([]pmessagingreliable.OutboxMessage, error) {
if a == nil || a.store == nil {
return nil, nil
}
events, err := a.store.ListPending(ctx, limit)
if err != nil {
return nil, err
}
result := make([]pmessagingreliable.OutboxMessage, 0, len(events))
for _, event := range events {
if event == nil {
continue
}
reference := ""
if eventRef := event.GetID(); eventRef != nil && !eventRef.IsZero() {
reference = eventRef.Hex()
}
result = append(result, pmessagingreliable.OutboxMessage{
Reference: reference,
EventID: strings.TrimSpace(event.EventID),
Subject: strings.TrimSpace(event.Subject),
Payload: append([]byte(nil), event.Payload...),
Attempts: event.Attempts,
OrganizationRef: event.OrganizationRef.Hex(),
CreatedAt: event.CreatedAt,
})
}
return result, nil
}
func (a *ledgerOutboxStoreAdapter) MarkSent(ctx context.Context, reference string, sentAt time.Time) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.MarkSent(ctx, eventRef, sentAt)
}
func (a *ledgerOutboxStoreAdapter) MarkFailed(ctx context.Context, reference string) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.MarkFailed(ctx, eventRef)
}
func (a *ledgerOutboxStoreAdapter) IncrementAttempts(ctx context.Context, reference string) error {
if a == nil || a.store == nil {
return nil
}
eventRef, err := parseObjectID(strings.TrimSpace(reference))
if err != nil {
return err
}
return a.store.IncrementAttempts(ctx, eventRef)
}
func ledgerOutboxDecoder(record pmessagingreliable.OutboxMessage) (me.Envelope, error) {
env, err := me.Deserialize(record.Payload)
if err == nil {
return env, nil
}
if strings.TrimSpace(record.Subject) != ledgerOutboxSubject {
return nil, err
}
return buildLedgerOutboxEnvelope(record.EventID, record.Payload, record.Attempts, record.OrganizationRef, record.CreatedAt)
}
func buildLedgerOutboxEnvelope(eventID string, payload []byte, attempts int, organizationRef string, createdAt time.Time) (me.Envelope, error) {
msg := ledgerOutboxMessage{
EventID: strings.TrimSpace(eventID),
Subject: ledgerOutboxSubject,
Payload: append([]byte(nil), payload...),
Attempts: attempts,
OrganizationRef: strings.TrimSpace(organizationRef),
CreatedAt: createdAt,
}
body, err := json.Marshal(msg)
if err != nil {
return nil, err
}
env := me.CreateEnvelope(outboxPublisherSender, domainmodel.NewNotification(mservice.LedgerOutbox, notification.NASent))
if _, err = env.Wrap(body); err != nil {
return nil, err
}
return env, nil
}

View File

@@ -12,33 +12,35 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tech/sendico/ledger/storage/model" "github.com/tech/sendico/ledger/storage/model"
me "github.com/tech/sendico/pkg/messaging/envelope" me "github.com/tech/sendico/pkg/messaging/envelope"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
func TestOutboxPublisherDispatchSuccess(t *testing.T) { func TestLedgerReliableProducerDispatchesLegacyOutboxRecords(t *testing.T) {
logger := zap.NewNop() logger := zap.NewNop()
event := &model.OutboxEvent{ event := &model.OutboxEvent{
EventID: "entry-1", EventID: "entry-1",
Subject: "ledger.entry.posted", Subject: ledgerOutboxSubject,
Payload: []byte(`{"journalEntryRef":"abc123"}`), Payload: []byte(`{"journalEntryRef":"abc123"}`),
Attempts: 0, Attempts: 0,
} }
event.SetID(bson.NewObjectID()) event.SetID(bson.NewObjectID())
event.OrganizationRef = bson.NewObjectID() event.OrganizationRef = bson.NewObjectID()
store := &recordingOutboxStore{ store := &recordingLedgerOutboxStore{
pending: []*model.OutboxEvent{event}, pending: []*model.OutboxEvent{event},
} }
producer := &stubProducer{} direct := &stubDirectProducer{}
publisher := newOutboxPublisher(logger, store, producer) producer, _, err := newLedgerReliableProducer(logger, direct, store, nil)
require.NoError(t, err)
processed, err := publisher.dispatchPending(context.Background()) processed, err := producer.DispatchPending(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, processed) assert.Equal(t, 1, processed)
require.Len(t, producer.envelopes, 1) require.Len(t, direct.envelopes, 1)
env := producer.envelopes[0] env := direct.envelopes[0]
assert.Equal(t, outboxPublisherSender, env.GetSender()) assert.Equal(t, outboxPublisherSender, env.GetSender())
assert.Equal(t, "ledger_outbox_sent", env.GetSignature().ToString()) assert.Equal(t, "ledger_outbox_sent", env.GetSignature().ToString())
@@ -47,6 +49,7 @@ func TestOutboxPublisherDispatchSuccess(t *testing.T) {
assert.Equal(t, event.EventID, message.EventID) assert.Equal(t, event.EventID, message.EventID)
assert.Equal(t, event.Subject, message.Subject) assert.Equal(t, event.Subject, message.Subject)
assert.Equal(t, event.OrganizationRef.Hex(), message.OrganizationRef) assert.Equal(t, event.OrganizationRef.Hex(), message.OrganizationRef)
assert.JSONEq(t, `{"journalEntryRef":"abc123"}`, string(message.Payload))
require.Len(t, store.markedSent, 1) require.Len(t, store.markedSent, 1)
assert.Equal(t, *event.GetID(), store.markedSent[0]) assert.Equal(t, *event.GetID(), store.markedSent[0])
@@ -54,37 +57,36 @@ func TestOutboxPublisherDispatchSuccess(t *testing.T) {
assert.Empty(t, store.incremented) assert.Empty(t, store.incremented)
} }
func TestOutboxPublisherDispatchFailureMarksAttempts(t *testing.T) { func TestLedgerReliableProducerMarksFailedOnDispatchError(t *testing.T) {
logger := zap.NewNop() logger := zap.NewNop()
event := &model.OutboxEvent{ event := &model.OutboxEvent{
EventID: "entry-2", EventID: "entry-2",
Subject: "ledger.entry.posted", Subject: ledgerOutboxSubject,
Payload: []byte(`{"journalEntryRef":"xyz789"}`), Payload: []byte(`{"journalEntryRef":"xyz789"}`),
Attempts: maxOutboxDeliveryAttempts - 1, Attempts: pmessagingreliable.DefaultSettings().MaxAttempts - 1,
} }
event.SetID(bson.NewObjectID()) event.SetID(bson.NewObjectID())
event.OrganizationRef = bson.NewObjectID() event.OrganizationRef = bson.NewObjectID()
store := &recordingOutboxStore{ store := &recordingLedgerOutboxStore{
pending: []*model.OutboxEvent{event}, pending: []*model.OutboxEvent{event},
} }
producer := &stubProducer{err: errors.New("publish failed")} direct := &stubDirectProducer{err: errors.New("publish failed")}
publisher := newOutboxPublisher(logger, store, producer) producer, _, err := newLedgerReliableProducer(logger, direct, store, nil)
require.NoError(t, err)
processed, err := publisher.dispatchPending(context.Background()) processed, err := producer.DispatchPending(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 1, processed) assert.Equal(t, 1, processed)
require.Len(t, store.incremented, 1) require.Len(t, store.incremented, 1)
assert.Equal(t, *event.GetID(), store.incremented[0]) assert.Equal(t, *event.GetID(), store.incremented[0])
require.Len(t, store.markedFailed, 1) require.Len(t, store.markedFailed, 1)
assert.Equal(t, *event.GetID(), store.markedFailed[0]) assert.Equal(t, *event.GetID(), store.markedFailed[0])
assert.Empty(t, store.markedSent) assert.Empty(t, store.markedSent)
} }
type recordingOutboxStore struct { type recordingLedgerOutboxStore struct {
mu sync.Mutex mu sync.Mutex
pending []*model.OutboxEvent pending []*model.OutboxEvent
@@ -94,11 +96,11 @@ type recordingOutboxStore struct {
incremented []bson.ObjectID incremented []bson.ObjectID
} }
func (s *recordingOutboxStore) Create(context.Context, *model.OutboxEvent) error { func (s *recordingLedgerOutboxStore) Create(context.Context, *model.OutboxEvent) error {
return nil return nil
} }
func (s *recordingOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) { func (s *recordingLedgerOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
events := s.pending events := s.pending
@@ -106,35 +108,34 @@ func (s *recordingOutboxStore) ListPending(context.Context, int) ([]*model.Outbo
return events, nil return events, nil
} }
func (s *recordingOutboxStore) MarkSent(_ context.Context, eventRef bson.ObjectID, sentAt time.Time) error { func (s *recordingLedgerOutboxStore) MarkSent(_ context.Context, eventRef bson.ObjectID, _ time.Time) error {
_ = sentAt
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.markedSent = append(s.markedSent, eventRef) s.markedSent = append(s.markedSent, eventRef)
return nil return nil
} }
func (s *recordingOutboxStore) MarkFailed(_ context.Context, eventRef bson.ObjectID) error { func (s *recordingLedgerOutboxStore) MarkFailed(_ context.Context, eventRef bson.ObjectID) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.markedFailed = append(s.markedFailed, eventRef) s.markedFailed = append(s.markedFailed, eventRef)
return nil return nil
} }
func (s *recordingOutboxStore) IncrementAttempts(_ context.Context, eventRef bson.ObjectID) error { func (s *recordingLedgerOutboxStore) IncrementAttempts(_ context.Context, eventRef bson.ObjectID) error {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
s.incremented = append(s.incremented, eventRef) s.incremented = append(s.incremented, eventRef)
return nil return nil
} }
type stubProducer struct { type stubDirectProducer struct {
mu sync.Mutex mu sync.Mutex
envelopes []me.Envelope envelopes []me.Envelope
err error err error
} }
func (p *stubProducer) SendMessage(env me.Envelope) error { func (p *stubDirectProducer) SendMessage(env me.Envelope) error {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
p.envelopes = append(p.envelopes, env) p.envelopes = append(p.envelopes, env)

View File

@@ -66,7 +66,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest("credit") recordDuplicateRequest("credit")
logger.Info("duplicate credit request (idempotency)", logger.Info("Duplicate credit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
@@ -76,7 +76,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
} }
if err != nil && err != storage.ErrJournalEntryNotFound { if err != nil && err != storage.ErrJournalEntryNotFound {
recordJournalEntryError("credit", "idempotency_check_failed") recordJournalEntryError("credit", "idempotency_check_failed")
logger.Warn("failed to check idempotency", zap.Error(err)) logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency") return nil, merrors.Internal("failed to check idempotency")
} }
@@ -99,7 +99,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
charges := req.Charges charges := req.Charges
if len(charges) == 0 { if len(charges) == 0 {
if computed, err := s.quoteFeesForCredit(ctx, req); err != nil { if computed, err := s.quoteFeesForCredit(ctx, req); err != nil {
logger.Warn("failed to quote fees", zap.Error(err)) logger.Warn("Failed to quote fees", zap.Error(err))
} else if len(computed) > 0 { } else if len(computed) > 0 {
charges = computed charges = computed
} }
@@ -133,7 +133,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -199,7 +199,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
entry.OrganizationRef = orgRef entry.OrganizationRef = orgRef
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
logger.Warn("failed to create journal entry", zap.Error(err)) logger.Warn("Failed to create journal entry", zap.Error(err))
return nil, merrors.Internal("failed to create journal entry") return nil, merrors.Internal("failed to create journal entry")
} }
@@ -217,7 +217,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
} }
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
logger.Warn("failed to create posting lines", zap.Error(err)) logger.Warn("Failed to create posting lines", zap.Error(err))
return nil, merrors.Internal("failed to create posting lines") return nil, merrors.Internal("failed to create posting lines")
} }

View File

@@ -64,7 +64,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest("debit") recordDuplicateRequest("debit")
logger.Info("duplicate debit request (idempotency)", logger.Info("Duplicate debit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
@@ -73,7 +73,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
}, nil }, nil
} }
if err != nil && err != storage.ErrJournalEntryNotFound { if err != nil && err != storage.ErrJournalEntryNotFound {
logger.Warn("failed to check idempotency", zap.Error(err)) logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency") return nil, merrors.Internal("failed to check idempotency")
} }
@@ -96,7 +96,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
charges := req.Charges charges := req.Charges
if len(charges) == 0 { if len(charges) == 0 {
if computed, err := s.quoteFeesForDebit(ctx, req); err != nil { if computed, err := s.quoteFeesForDebit(ctx, req); err != nil {
logger.Warn("failed to quote fees", zap.Error(err)) logger.Warn("Failed to quote fees", zap.Error(err))
} else if len(computed) > 0 { } else if len(computed) > 0 {
charges = computed charges = computed
} }
@@ -130,7 +130,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -196,7 +196,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
entry.OrganizationRef = orgRef entry.OrganizationRef = orgRef
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
logger.Warn("failed to create journal entry", zap.Error(err)) logger.Warn("Failed to create journal entry", zap.Error(err))
return nil, merrors.Internal("failed to create journal entry") return nil, merrors.Internal("failed to create journal entry")
} }
@@ -214,7 +214,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
} }
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
logger.Warn("failed to create posting lines", zap.Error(err)) logger.Warn("Failed to create posting lines", zap.Error(err))
return nil, merrors.Internal("failed to create posting lines") return nil, merrors.Internal("failed to create posting lines")
} }

View File

@@ -61,7 +61,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest("credit") recordDuplicateRequest("credit")
logger.Info("duplicate external credit request (idempotency)", logger.Info("Duplicate external credit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
@@ -71,7 +71,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
} }
if err != nil && err != storage.ErrJournalEntryNotFound { if err != nil && err != storage.ErrJournalEntryNotFound {
recordJournalEntryError("credit", "idempotency_check_failed") recordJournalEntryError("credit", "idempotency_check_failed")
logger.Warn("failed to check idempotency", zap.Error(err)) logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency") return nil, merrors.Internal("failed to check idempotency")
} }
@@ -113,7 +113,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
charges := req.Charges charges := req.Charges
if len(charges) == 0 { if len(charges) == 0 {
if computed, err := s.quoteFeesForCredit(ctx, req); err != nil { if computed, err := s.quoteFeesForCredit(ctx, req); err != nil {
logger.Warn("failed to quote fees", zap.Error(err)) logger.Warn("Failed to quote fees", zap.Error(err))
} else if len(computed) > 0 { } else if len(computed) > 0 {
charges = computed charges = computed
} }
@@ -147,7 +147,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -202,7 +202,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
entry.OrganizationRef = orgRef entry.OrganizationRef = orgRef
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
logger.Warn("failed to create journal entry", zap.Error(err)) logger.Warn("Failed to create journal entry", zap.Error(err))
return nil, merrors.Internal("failed to create journal entry") return nil, merrors.Internal("failed to create journal entry")
} }
@@ -220,7 +220,7 @@ func (s *Service) postExternalCreditResponder(_ context.Context, req *ledgerv1.P
} }
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
logger.Warn("failed to create posting lines", zap.Error(err)) logger.Warn("Failed to create posting lines", zap.Error(err))
return nil, merrors.Internal("failed to create posting lines") return nil, merrors.Internal("failed to create posting lines")
} }
@@ -294,7 +294,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest("debit") recordDuplicateRequest("debit")
logger.Info("duplicate external debit request (idempotency)", logger.Info("Duplicate external debit request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
@@ -304,7 +304,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
} }
if err != nil && err != storage.ErrJournalEntryNotFound { if err != nil && err != storage.ErrJournalEntryNotFound {
recordJournalEntryError("debit", "idempotency_check_failed") recordJournalEntryError("debit", "idempotency_check_failed")
logger.Warn("failed to check idempotency", zap.Error(err)) logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency") return nil, merrors.Internal("failed to check idempotency")
} }
@@ -346,7 +346,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
charges := req.Charges charges := req.Charges
if len(charges) == 0 { if len(charges) == 0 {
if computed, err := s.quoteFeesForDebit(ctx, req); err != nil { if computed, err := s.quoteFeesForDebit(ctx, req); err != nil {
logger.Warn("failed to quote fees", zap.Error(err)) logger.Warn("Failed to quote fees", zap.Error(err))
} else if len(computed) > 0 { } else if len(computed) > 0 {
charges = computed charges = computed
} }
@@ -380,7 +380,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -435,7 +435,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
entry.OrganizationRef = orgRef entry.OrganizationRef = orgRef
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
logger.Warn("failed to create journal entry", zap.Error(err)) logger.Warn("Failed to create journal entry", zap.Error(err))
return nil, merrors.Internal("failed to create journal entry") return nil, merrors.Internal("failed to create journal entry")
} }
@@ -453,7 +453,7 @@ func (s *Service) postExternalDebitResponder(_ context.Context, req *ledgerv1.Po
} }
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
logger.Warn("failed to create posting lines", zap.Error(err)) logger.Warn("Failed to create posting lines", zap.Error(err))
return nil, merrors.Internal("failed to create posting lines") return nil, merrors.Internal("failed to create posting lines")
} }

View File

@@ -77,7 +77,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest("fx") recordDuplicateRequest("fx")
logger.Info("duplicate FX request (idempotency)", logger.Info("Duplicate FX request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
@@ -86,7 +86,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
}, nil }, nil
} }
if err != nil && err != storage.ErrJournalEntryNotFound { if err != nil && err != storage.ErrJournalEntryNotFound {
logger.Warn("failed to check idempotency", zap.Error(err)) logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency") return nil, merrors.Internal("failed to check idempotency")
} }
@@ -96,7 +96,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData("from_account not found") return nil, merrors.NoData("from_account not found")
} }
logger.Warn("failed to get from_account", zap.Error(err)) logger.Warn("Failed to get from_account", zap.Error(err))
return nil, merrors.Internal("failed to get from_account") return nil, merrors.Internal("failed to get from_account")
} }
if err := validateAccountForOrg(fromAccount, orgRef, req.FromMoney.Currency); err != nil { if err := validateAccountForOrg(fromAccount, orgRef, req.FromMoney.Currency); err != nil {
@@ -108,7 +108,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData("to_account not found") return nil, merrors.NoData("to_account not found")
} }
logger.Warn("failed to get to_account", zap.Error(err)) logger.Warn("Failed to get to_account", zap.Error(err))
return nil, merrors.Internal("failed to get to_account") return nil, merrors.Internal("failed to get to_account")
} }
if err := validateAccountForOrg(toAccount, orgRef, req.ToMoney.Currency); err != nil { if err := validateAccountForOrg(toAccount, orgRef, req.ToMoney.Currency); err != nil {
@@ -162,7 +162,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("failed to get FX charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get FX charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -210,7 +210,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
entry.OrganizationRef = orgRef entry.OrganizationRef = orgRef
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
logger.Warn("failed to create journal entry", zap.Error(err)) logger.Warn("Failed to create journal entry", zap.Error(err))
return nil, merrors.Internal("failed to create journal entry") return nil, merrors.Internal("failed to create journal entry")
} }
@@ -224,7 +224,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
} }
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
logger.Warn("failed to create posting lines", zap.Error(err)) logger.Warn("Failed to create posting lines", zap.Error(err))
return nil, merrors.Internal("failed to create posting lines") return nil, merrors.Internal("failed to create posting lines")
} }

View File

@@ -139,7 +139,7 @@ func (s *Service) resolveSettlementAccount(ctx context.Context, orgRef bson.Obje
if errors.Is(err, storage.ErrAccountNotFound) { if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.NoData("contra account not found") return nil, merrors.NoData("contra account not found")
} }
s.logger.Warn("failed to load override contra account", zap.Error(err), zap.String("accountRef", overrideRef.Hex())) s.logger.Warn("Failed to load override contra account", zap.Error(err), zap.String("accountRef", overrideRef.Hex()))
return nil, merrors.Internal("failed to load contra account") return nil, merrors.Internal("failed to load contra account")
} }
if err := validateAccountForOrg(account, orgRef, currency); err != nil { if err := validateAccountForOrg(account, orgRef, currency); err != nil {
@@ -153,7 +153,7 @@ func (s *Service) resolveSettlementAccount(ctx context.Context, orgRef bson.Obje
if errors.Is(err, storage.ErrAccountNotFound) { if errors.Is(err, storage.ErrAccountNotFound) {
return nil, merrors.InvalidArgument("no default settlement account configured for currency") return nil, merrors.InvalidArgument("no default settlement account configured for currency")
} }
s.logger.Warn("failed to resolve default settlement account", s.logger.Warn("Failed to resolve default settlement account",
zap.Error(err), zap.Error(err),
mzap.ObjRef("organization_ref", orgRef), mzap.ObjRef("organization_ref", orgRef),
zap.String("currency", currency)) zap.String("currency", currency))
@@ -197,13 +197,13 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
for accountRef, delta := range balanceDeltas { for accountRef, delta := range balanceDeltas {
account := accounts[accountRef] account := accounts[accountRef]
if account == nil { if account == nil {
s.logger.Warn("account cache missing for balance update", mzap.AccRef(accountRef)) s.logger.Warn("Account cache missing for balance update", mzap.AccRef(accountRef))
return merrors.Internal("account cache missing for balance update") return merrors.Internal("account cache missing for balance update")
} }
currentBalance, err := balancesStore.Get(ctx, accountRef) currentBalance, err := balancesStore.Get(ctx, accountRef)
if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) { if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) {
s.logger.Warn("failed to fetch account balance", s.logger.Warn("Failed to fetch account balance",
zap.Error(err), zap.Error(err),
mzap.AccRef(accountRef)) mzap.AccRef(accountRef))
return merrors.Internal("failed to update balance") return merrors.Internal("failed to update balance")
@@ -238,7 +238,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
} }
if err := balancesStore.Upsert(ctx, newBalance); err != nil { if err := balancesStore.Upsert(ctx, newBalance); err != nil {
s.logger.Warn("failed to upsert account balance", zap.Error(err), mzap.AccRef(accountRef)) s.logger.Warn("Failed to upsert account balance", zap.Error(err), mzap.AccRef(accountRef))
return merrors.Internal("failed to update balance") return merrors.Internal("failed to update balance")
} }
} }
@@ -275,21 +275,26 @@ func (s *Service) enqueueOutbox(ctx context.Context, entry *model.JournalEntry,
body, err := json.Marshal(payload) body, err := json.Marshal(payload)
if err != nil { if err != nil {
s.logger.Warn("failed to marshal ledger outbox payload", zap.Error(err)) s.logger.Warn("Failed to marshal ledger outbox payload", zap.Error(err))
return merrors.Internal("failed to marshal ledger event") return merrors.Internal("failed to marshal ledger event")
} }
envelope, err := buildLedgerOutboxEnvelope(entryID.Hex(), body, 0, entry.OrganizationRef.Hex(), time.Now().UTC())
event := &model.OutboxEvent{ if err != nil {
EventID: entryID.Hex(), s.logger.Warn("Failed to build ledger outbox envelope", zap.Error(err))
Subject: ledgerOutboxSubject, return merrors.Internal("failed to prepare ledger event envelope")
Payload: body,
Status: model.OutboxStatusPending,
Attempts: 0,
} }
event.OrganizationRef = entry.OrganizationRef
if err := s.storage.Outbox().Create(ctx, event); err != nil { if err := s.startOutboxReliableProducer(); err != nil {
s.logger.Warn("failed to enqueue ledger outbox event", zap.Error(err)) s.logger.Warn("Failed to initialise outbox reliable producer", zap.Error(err))
return merrors.Internal("failed to initialize reliable outbox")
}
if s.outbox.producer == nil {
s.logger.Warn("Failed to enqueue ledger outbox event: reliable producer not configured")
return merrors.Internal("failed to enqueue ledger event")
}
if err := s.outbox.producer.SendWithOutbox(ctx, envelope); err != nil {
s.logger.Warn("Failed to enqueue ledger outbox event", zap.Error(err))
return merrors.Internal("failed to enqueue ledger event") return merrors.Internal("failed to enqueue ledger event")
} }

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/ledger/storage" "github.com/tech/sendico/ledger/storage"
"github.com/tech/sendico/ledger/storage/model" "github.com/tech/sendico/ledger/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
me "github.com/tech/sendico/pkg/messaging/envelope"
pmodel "github.com/tech/sendico/pkg/model" pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role" "github.com/tech/sendico/pkg/model/account_role"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@@ -278,11 +279,20 @@ func TestEnqueueOutbox_CreatesEvent(t *testing.T) {
require.NoError(t, service.enqueueOutbox(ctx, entry, lines)) require.NoError(t, service.enqueueOutbox(ctx, entry, lines))
require.Len(t, producer.created, 1) require.Len(t, producer.created, 1)
event := producer.created[0] event := producer.created[0]
assert.Equal(t, entryID.Hex(), event.EventID) assert.Equal(t, "ledger_outbox_sent", event.Subject)
assert.Equal(t, ledgerOutboxSubject, event.Subject)
envelope, err := me.Deserialize(event.Payload)
require.NoError(t, err)
assert.Equal(t, outboxPublisherSender, envelope.GetSender())
assert.Equal(t, "ledger_outbox_sent", envelope.GetSignature().ToString())
var wrapped ledgerOutboxMessage
require.NoError(t, json.Unmarshal(envelope.GetData(), &wrapped))
assert.Equal(t, entryID.Hex(), wrapped.EventID)
assert.Equal(t, ledgerOutboxSubject, wrapped.Subject)
var payload outboxJournalPayload var payload outboxJournalPayload
require.NoError(t, json.Unmarshal(event.Payload, &payload)) require.NoError(t, json.Unmarshal(wrapped.Payload, &payload))
assert.Equal(t, entryID.Hex(), payload.JournalEntryRef) assert.Equal(t, entryID.Hex(), payload.JournalEntryRef)
assert.Equal(t, "credit", payload.EntryType) assert.Equal(t, "credit", payload.EntryType)
assert.Len(t, payload.Lines, 1) assert.Len(t, payload.Lines, 1)

View File

@@ -87,7 +87,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
if err == nil && existingEntry != nil { if err == nil && existingEntry != nil {
recordDuplicateRequest("transfer") recordDuplicateRequest("transfer")
logger.Info("duplicate transfer request (idempotency)", logger.Info("Duplicate transfer request (idempotency)",
zap.String("existingEntryID", existingEntry.GetID().Hex())) zap.String("existingEntryID", existingEntry.GetID().Hex()))
return &ledgerv1.PostResponse{ return &ledgerv1.PostResponse{
JournalEntryRef: existingEntry.GetID().Hex(), JournalEntryRef: existingEntry.GetID().Hex(),
@@ -96,7 +96,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
}, nil }, nil
} }
if err != nil && err != storage.ErrJournalEntryNotFound { if err != nil && err != storage.ErrJournalEntryNotFound {
logger.Warn("failed to check idempotency", zap.Error(err)) logger.Warn("Failed to check idempotency", zap.Error(err))
return nil, merrors.Internal("failed to check idempotency") return nil, merrors.Internal("failed to check idempotency")
} }
@@ -172,7 +172,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
} }
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) logger.Warn("Failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
return nil, merrors.Internal("failed to get charge account") return nil, merrors.Internal("failed to get charge account")
} }
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
@@ -208,7 +208,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
entry.OrganizationRef = orgRef entry.OrganizationRef = orgRef
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
logger.Warn("failed to create journal entry", zap.Error(err)) logger.Warn("Failed to create journal entry", zap.Error(err))
return nil, merrors.Internal("failed to create journal entry") return nil, merrors.Internal("failed to create journal entry")
} }
@@ -226,7 +226,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
} }
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
logger.Warn("failed to create posting lines", zap.Error(err)) logger.Warn("Failed to create posting lines", zap.Error(err))
return nil, merrors.Internal("failed to create posting lines") return nil, merrors.Internal("failed to create posting lines")
} }

View File

@@ -36,7 +36,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData("account not found") return nil, merrors.NoData("account not found")
} }
logger.Warn("failed to get account", zap.Error(err)) logger.Warn("Failed to get account", zap.Error(err))
return nil, merrors.Internal("failed to get account") return nil, merrors.Internal("failed to get account")
} }
@@ -55,7 +55,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
LastUpdated: timestamppb.Now(), LastUpdated: timestamppb.Now(),
}, nil }, nil
} }
logger.Warn("failed to get balance", zap.Error(err)) logger.Warn("Failed to get balance", zap.Error(err))
return nil, merrors.Internal("failed to get balance") return nil, merrors.Internal("failed to get balance")
} }
@@ -92,14 +92,14 @@ func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetE
if err == storage.ErrJournalEntryNotFound { if err == storage.ErrJournalEntryNotFound {
return nil, merrors.NoData("journal entry not found") return nil, merrors.NoData("journal entry not found")
} }
logger.Warn("failed to get journal entry", zap.Error(err)) logger.Warn("Failed to get journal entry", zap.Error(err))
return nil, merrors.Internal("failed to get journal entry") return nil, merrors.Internal("failed to get journal entry")
} }
// Get posting lines for this entry // Get posting lines for this entry
lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef) lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
if err != nil { if err != nil {
logger.Warn("failed to get posting lines", zap.Error(err)) logger.Warn("Failed to get posting lines", zap.Error(err))
return nil, merrors.Internal("failed to get posting lines") return nil, merrors.Internal("failed to get posting lines")
} }
@@ -151,7 +151,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
if err == storage.ErrAccountNotFound { if err == storage.ErrAccountNotFound {
return nil, merrors.NoData("account not found") return nil, merrors.NoData("account not found")
} }
logger.Warn("failed to get account", zap.Error(err)) logger.Warn("Failed to get account", zap.Error(err))
return nil, merrors.Internal("failed to get account") return nil, merrors.Internal("failed to get account")
} }
@@ -176,7 +176,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
// Get posting lines for account // Get posting lines for account
postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset) postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset)
if err != nil { if err != nil {
logger.Warn("failed to get posting lines", zap.Error(err)) logger.Warn("Failed to get posting lines", zap.Error(err))
return nil, merrors.Internal("failed to get posting lines") return nil, merrors.Internal("failed to get posting lines")
} }
@@ -196,20 +196,20 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
for entryRefHex := range entryMap { for entryRefHex := range entryMap {
entryRef, err := parseObjectID(entryRefHex) entryRef, err := parseObjectID(entryRefHex)
if err != nil { if err != nil {
s.logger.Warn("invalid journal entry ref in posting lines", zap.String("entry_ref", entryRefHex), zap.Error(err)) s.logger.Warn("Invalid journal entry ref in posting lines", zap.String("entry_ref", entryRefHex), zap.Error(err))
return nil, err return nil, err
} }
entry, err := s.storage.JournalEntries().Get(ctx, entryRef) entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
if err != nil { if err != nil {
logger.Warn("failed to get journal entry for statement", zap.Error(err), zap.String("entry_ref", entryRefHex)) logger.Warn("Failed to get journal entry for statement", zap.Error(err), zap.String("entry_ref", entryRefHex))
continue continue
} }
// Get all lines for this entry // Get all lines for this entry
lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef) lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
if err != nil { if err != nil {
logger.Warn("failed to get posting lines for entry", zap.Error(err), zap.String("entry_ref", entryRefHex)) logger.Warn("Failed to get posting lines for entry", zap.Error(err), zap.String("entry_ref", entryRefHex))
continue continue
} }

View File

@@ -23,6 +23,7 @@ import (
"github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
pmessaging "github.com/tech/sendico/pkg/messaging" pmessaging "github.com/tech/sendico/pkg/messaging"
pmessagingreliable "github.com/tech/sendico/pkg/messaging/reliable"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
pmodel "github.com/tech/sendico/pkg/model" pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
@@ -44,14 +45,15 @@ type Service struct {
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
producer pmessaging.Producer producer pmessaging.Producer
msgCfg pmodel.SettingsT
fees feesDependency fees feesDependency
announcer *discovery.Announcer announcer *discovery.Announcer
invokeURI string invokeURI string
outbox struct { outbox struct {
once sync.Once once sync.Once
cancel context.CancelFunc cancel context.CancelFunc
publisher *outboxPublisher producer *pmessagingreliable.ReliableProducer
} }
systemAccounts struct { systemAccounts struct {
@@ -70,7 +72,7 @@ func (f feesDependency) available() bool {
return f.client != nil return f.client != nil
} }
func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, feesClient feesv1.FeeEngineClient, feesTimeout time.Duration, invokeURI string) *Service { func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, msgCfg pmodel.SettingsT, feesClient feesv1.FeeEngineClient, feesTimeout time.Duration, invokeURI string) (*Service, error) {
// Initialize Prometheus metrics // Initialize Prometheus metrics
initMetrics() initMetrics()
@@ -78,6 +80,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
logger: logger.Named("ledger"), logger: logger.Named("ledger"),
storage: repo, storage: repo,
producer: prod, producer: prod,
msgCfg: msgCfg,
invokeURI: strings.TrimSpace(invokeURI), invokeURI: strings.TrimSpace(invokeURI),
fees: feesDependency{ fees: feesDependency{
client: feesClient, client: feesClient,
@@ -85,9 +88,11 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
}, },
} }
service.startOutboxPublisher() if err := service.startOutboxReliableProducer(); err != nil {
return nil, err
}
service.startDiscoveryAnnouncer() service.startDiscoveryAnnouncer()
return service return service, nil
} }
func (s *Service) Register(router routers.GRPC) error { func (s *Service) Register(router routers.GRPC) error {
@@ -405,23 +410,39 @@ func (s *Service) startDiscoveryAnnouncer() {
s.announcer.Start() s.announcer.Start()
} }
func (s *Service) startOutboxPublisher() { func (s *Service) startOutboxReliableProducer() error {
if s.storage == nil || s.producer == nil { if s.storage == nil {
return return nil
} }
var initErr error
s.outbox.once.Do(func() { s.outbox.once.Do(func() {
outboxStore := s.storage.Outbox() outboxStore := s.storage.Outbox()
if outboxStore == nil { if outboxStore == nil {
return return
} }
reliableProducer, settings, err := newLedgerReliableProducer(s.logger, s.producer, outboxStore, s.msgCfg)
if err != nil {
initErr = err
return
}
s.outbox.producer = reliableProducer
if s.outbox.producer == nil || s.producer == nil {
s.logger.Info("Outbox reliable publisher disabled",
zap.Bool("enabled", settings.Enabled))
return
}
s.logger.Info("Outbox reliable publisher configured",
zap.Bool("enabled", settings.Enabled),
zap.Int("batch_size", settings.BatchSize),
zap.Int("poll_interval_seconds", settings.PollIntervalSeconds),
zap.Int("max_attempts", settings.MaxAttempts))
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
s.outbox.cancel = cancel s.outbox.cancel = cancel
s.outbox.publisher = newOutboxPublisher(s.logger, outboxStore, s.producer) go s.outbox.producer.Run(ctx)
go s.outbox.publisher.run(ctx)
}) })
return initErr
} }
// BlockAccount freezes a ledger account // BlockAccount freezes a ledger account

View File

@@ -32,12 +32,12 @@ func NewBalances(logger mlogger.Logger, db *mongo.Database) (storage.BalancesSto
Unique: true, Unique: true,
} }
if err := repo.CreateIndex(uniqueIndex); err != nil { if err := repo.CreateIndex(uniqueIndex); err != nil {
logger.Error("failed to ensure balances unique index", zap.Error(err)) logger.Error("Failed to ensure balances unique index", zap.Error(err))
return nil, err return nil, err
} }
childLogger := logger.Named(model.AccountBalancesCollection) childLogger := logger.Named(model.AccountBalancesCollection)
childLogger.Debug("balances store initialised", zap.String("collection", model.AccountBalancesCollection)) childLogger.Debug("Balances store initialised", zap.String("collection", model.AccountBalancesCollection))
return &balancesStore{ return &balancesStore{
logger: childLogger, logger: childLogger,
@@ -47,7 +47,7 @@ func NewBalances(logger mlogger.Logger, db *mongo.Database) (storage.BalancesSto
func (b *balancesStore) Get(ctx context.Context, accountRef bson.ObjectID) (*model.AccountBalance, error) { func (b *balancesStore) Get(ctx context.Context, accountRef bson.ObjectID) (*model.AccountBalance, error) {
if accountRef.IsZero() { if accountRef.IsZero() {
b.logger.Warn("attempt to get balance with zero account ID") b.logger.Warn("Attempt to get balance with zero account ID")
return nil, merrors.InvalidArgument("balancesStore: zero account ID") return nil, merrors.InvalidArgument("balancesStore: zero account ID")
} }
@@ -56,25 +56,25 @@ func (b *balancesStore) Get(ctx context.Context, accountRef bson.ObjectID) (*mod
result := &model.AccountBalance{} result := &model.AccountBalance{}
if err := b.repo.FindOneByFilter(ctx, query, result); err != nil { if err := b.repo.FindOneByFilter(ctx, query, result); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
b.logger.Debug("balance not found", mzap.AccRef(accountRef)) b.logger.Debug("Balance not found", mzap.AccRef(accountRef))
return nil, storage.ErrBalanceNotFound return nil, storage.ErrBalanceNotFound
} }
b.logger.Warn("failed to get balance", zap.Error(err), mzap.AccRef(accountRef)) b.logger.Warn("Failed to get balance", zap.Error(err), mzap.AccRef(accountRef))
return nil, err return nil, err
} }
b.logger.Debug("balance loaded", mzap.AccRef(accountRef), b.logger.Debug("Balance loaded", mzap.AccRef(accountRef),
zap.String("balance", result.Balance)) zap.String("balance", result.Balance))
return result, nil return result, nil
} }
func (b *balancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error { func (b *balancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error {
if balance == nil { if balance == nil {
b.logger.Warn("attempt to upsert nil balance") b.logger.Warn("Attempt to upsert nil balance")
return merrors.InvalidArgument("balancesStore: nil balance") return merrors.InvalidArgument("balancesStore: nil balance")
} }
if balance.AccountRef.IsZero() { if balance.AccountRef.IsZero() {
b.logger.Warn("attempt to upsert balance with zero account ID") b.logger.Warn("Attempt to upsert balance with zero account ID")
return merrors.InvalidArgument("balancesStore: zero account ID") return merrors.InvalidArgument("balancesStore: zero account ID")
} }
@@ -83,24 +83,24 @@ func (b *balancesStore) Upsert(ctx context.Context, balance *model.AccountBalanc
if err := b.repo.FindOneByFilter(ctx, filter, existing); err != nil { if err := b.repo.FindOneByFilter(ctx, filter, existing); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
b.logger.Debug("inserting new balance", zap.String("accountRef", balance.AccountRef.Hex())) b.logger.Debug("Inserting new balance", zap.String("accountRef", balance.AccountRef.Hex()))
return b.repo.Insert(ctx, balance, filter) return b.repo.Insert(ctx, balance, filter)
} }
b.logger.Warn("failed to fetch balance", zap.Error(err), zap.String("accountRef", balance.AccountRef.Hex())) b.logger.Warn("Failed to fetch balance", zap.Error(err), zap.String("accountRef", balance.AccountRef.Hex()))
return err return err
} }
if existing.GetID() != nil { if existing.GetID() != nil {
balance.SetID(*existing.GetID()) balance.SetID(*existing.GetID())
} }
b.logger.Debug("updating balance", zap.String("accountRef", balance.AccountRef.Hex()), b.logger.Debug("Updating balance", zap.String("accountRef", balance.AccountRef.Hex()),
zap.String("balance", balance.Balance)) zap.String("balance", balance.Balance))
return b.repo.Update(ctx, balance) return b.repo.Update(ctx, balance)
} }
func (b *balancesStore) IncrementBalance(ctx context.Context, accountRef bson.ObjectID, amount string) error { func (b *balancesStore) IncrementBalance(ctx context.Context, accountRef bson.ObjectID, amount string) error {
if accountRef.IsZero() { if accountRef.IsZero() {
b.logger.Warn("attempt to increment balance with zero account ID") b.logger.Warn("Attempt to increment balance with zero account ID")
return merrors.InvalidArgument("balancesStore: zero account ID") return merrors.InvalidArgument("balancesStore: zero account ID")
} }

View File

@@ -33,7 +33,7 @@ func NewJournalEntries(logger mlogger.Logger, db *mongo.Database) (storage.Journ
Unique: true, Unique: true,
} }
if err := repo.CreateIndex(uniqueIndex); err != nil { if err := repo.CreateIndex(uniqueIndex); err != nil {
logger.Error("failed to ensure journal entries idempotency index", zap.Error(err)) logger.Error("Failed to ensure journal entries idempotency index", zap.Error(err))
return nil, err return nil, err
} }
@@ -45,12 +45,12 @@ func NewJournalEntries(logger mlogger.Logger, db *mongo.Database) (storage.Journ
}, },
} }
if err := repo.CreateIndex(orgIndex); err != nil { if err := repo.CreateIndex(orgIndex); err != nil {
logger.Error("failed to ensure journal entries organization index", zap.Error(err)) logger.Error("Failed to ensure journal entries organization index", zap.Error(err))
return nil, err return nil, err
} }
childLogger := logger.Named(model.JournalEntriesCollection) childLogger := logger.Named(model.JournalEntriesCollection)
childLogger.Debug("journal entries store initialised", zap.String("collection", model.JournalEntriesCollection)) childLogger.Debug("Journal entries store initialised", zap.String("collection", model.JournalEntriesCollection))
return &journalEntriesStore{ return &journalEntriesStore{
logger: childLogger, logger: childLogger,
@@ -60,52 +60,52 @@ func NewJournalEntries(logger mlogger.Logger, db *mongo.Database) (storage.Journ
func (j *journalEntriesStore) Create(ctx context.Context, entry *model.JournalEntry) error { func (j *journalEntriesStore) Create(ctx context.Context, entry *model.JournalEntry) error {
if entry == nil { if entry == nil {
j.logger.Warn("attempt to create nil journal entry") j.logger.Warn("Attempt to create nil journal entry")
return merrors.InvalidArgument("journalEntriesStore: nil journal entry") return merrors.InvalidArgument("journalEntriesStore: nil journal entry")
} }
if err := j.repo.Insert(ctx, entry, nil); err != nil { if err := j.repo.Insert(ctx, entry, nil); err != nil {
if mongo.IsDuplicateKeyError(err) { if mongo.IsDuplicateKeyError(err) {
j.logger.Warn("duplicate idempotency key", zap.String("idempotency_key", entry.IdempotencyKey)) j.logger.Warn("Duplicate idempotency key", zap.String("idempotency_key", entry.IdempotencyKey))
return storage.ErrDuplicateIdempotency return storage.ErrDuplicateIdempotency
} }
j.logger.Warn("failed to create journal entry", zap.Error(err)) j.logger.Warn("Failed to create journal entry", zap.Error(err))
return err return err
} }
j.logger.Debug("journal entry created", zap.String("idempotency_key", entry.IdempotencyKey), j.logger.Debug("Journal entry created", zap.String("idempotency_key", entry.IdempotencyKey),
zap.String("entryType", string(entry.EntryType))) zap.String("entryType", string(entry.EntryType)))
return nil return nil
} }
func (j *journalEntriesStore) Get(ctx context.Context, entryRef bson.ObjectID) (*model.JournalEntry, error) { func (j *journalEntriesStore) Get(ctx context.Context, entryRef bson.ObjectID) (*model.JournalEntry, error) {
if entryRef.IsZero() { if entryRef.IsZero() {
j.logger.Warn("attempt to get journal entry with zero ID") j.logger.Warn("Attempt to get journal entry with zero ID")
return nil, merrors.InvalidArgument("journalEntriesStore: zero entry ID") return nil, merrors.InvalidArgument("journalEntriesStore: zero entry ID")
} }
result := &model.JournalEntry{} result := &model.JournalEntry{}
if err := j.repo.Get(ctx, entryRef, result); err != nil { if err := j.repo.Get(ctx, entryRef, result); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
j.logger.Debug("journal entry not found", mzap.ObjRef("entry_ref", entryRef)) j.logger.Debug("Journal entry not found", mzap.ObjRef("entry_ref", entryRef))
return nil, storage.ErrJournalEntryNotFound return nil, storage.ErrJournalEntryNotFound
} }
j.logger.Warn("failed to get journal entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef)) j.logger.Warn("Failed to get journal entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef))
return nil, err return nil, err
} }
j.logger.Debug("journal entry loaded", mzap.ObjRef("entry_ref", entryRef), j.logger.Debug("Journal entry loaded", mzap.ObjRef("entry_ref", entryRef),
zap.String("idempotency_key", result.IdempotencyKey)) zap.String("idempotency_key", result.IdempotencyKey))
return result, nil return result, nil
} }
func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.JournalEntry, error) { func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.JournalEntry, error) {
if orgRef.IsZero() { if orgRef.IsZero() {
j.logger.Warn("attempt to get journal entry with zero organization ID") j.logger.Warn("Attempt to get journal entry with zero organization ID")
return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID") return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID")
} }
if idempotencyKey == "" { if idempotencyKey == "" {
j.logger.Warn("attempt to get journal entry with empty idempotency key") j.logger.Warn("Attempt to get journal entry with empty idempotency key")
return nil, merrors.InvalidArgument("journalEntriesStore: empty idempotency key") return nil, merrors.InvalidArgument("journalEntriesStore: empty idempotency key")
} }
@@ -116,21 +116,21 @@ func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef bs
result := &model.JournalEntry{} result := &model.JournalEntry{}
if err := j.repo.FindOneByFilter(ctx, query, result); err != nil { if err := j.repo.FindOneByFilter(ctx, query, result); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
j.logger.Debug("journal entry not found by idempotency key", zap.String("idempotency_key", idempotencyKey)) j.logger.Debug("Journal entry not found by idempotency key", zap.String("idempotency_key", idempotencyKey))
return nil, storage.ErrJournalEntryNotFound return nil, storage.ErrJournalEntryNotFound
} }
j.logger.Warn("failed to get journal entry by idempotency key", zap.Error(err), j.logger.Warn("Failed to get journal entry by idempotency key", zap.Error(err),
zap.String("idempotency_key", idempotencyKey)) zap.String("idempotency_key", idempotencyKey))
return nil, err return nil, err
} }
j.logger.Debug("journal entry loaded by idempotency key", zap.String("idempotency_key", idempotencyKey)) j.logger.Debug("Journal entry loaded by idempotency key", zap.String("idempotency_key", idempotencyKey))
return result, nil return result, nil
} }
func (j *journalEntriesStore) ListByOrganization(ctx context.Context, orgRef bson.ObjectID, limit int, offset int) ([]*model.JournalEntry, error) { func (j *journalEntriesStore) ListByOrganization(ctx context.Context, orgRef bson.ObjectID, limit int, offset int) ([]*model.JournalEntry, error) {
if orgRef.IsZero() { if orgRef.IsZero() {
j.logger.Warn("attempt to list journal entries with zero organization ID") j.logger.Warn("Attempt to list journal entries with zero organization ID")
return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID") return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID")
} }
@@ -152,10 +152,10 @@ func (j *journalEntriesStore) ListByOrganization(ctx context.Context, orgRef bso
return nil return nil
}) })
if err != nil { if err != nil {
j.logger.Warn("failed to list journal entries", zap.Error(err)) j.logger.Warn("Failed to list journal entries", zap.Error(err))
return nil, err return nil, err
} }
j.logger.Debug("listed journal entries", zap.Int("count", len(entries))) j.logger.Debug("Listed journal entries", zap.Int("count", len(entries)))
return entries, nil return entries, nil
} }

View File

@@ -31,7 +31,7 @@ func NewOutbox(logger mlogger.Logger, db *mongo.Database) (storage.OutboxStore,
}, },
} }
if err := repo.CreateIndex(statusIndex); err != nil { if err := repo.CreateIndex(statusIndex); err != nil {
logger.Error("failed to ensure outbox status index", zap.Error(err)) logger.Error("Failed to ensure outbox status index", zap.Error(err))
return nil, err return nil, err
} }
@@ -43,12 +43,12 @@ func NewOutbox(logger mlogger.Logger, db *mongo.Database) (storage.OutboxStore,
Unique: true, Unique: true,
} }
if err := repo.CreateIndex(eventIdIndex); err != nil { if err := repo.CreateIndex(eventIdIndex); err != nil {
logger.Error("failed to ensure outbox eventId index", zap.Error(err)) logger.Error("Failed to ensure outbox eventId index", zap.Error(err))
return nil, err return nil, err
} }
childLogger := logger.Named(model.OutboxCollection) childLogger := logger.Named(model.OutboxCollection)
childLogger.Debug("outbox store initialised", zap.String("collection", model.OutboxCollection)) childLogger.Debug("Outbox store initialised", zap.String("collection", model.OutboxCollection))
return &outboxStore{ return &outboxStore{
logger: childLogger, logger: childLogger,
@@ -58,20 +58,20 @@ func NewOutbox(logger mlogger.Logger, db *mongo.Database) (storage.OutboxStore,
func (o *outboxStore) Create(ctx context.Context, event *model.OutboxEvent) error { func (o *outboxStore) Create(ctx context.Context, event *model.OutboxEvent) error {
if event == nil { if event == nil {
o.logger.Warn("attempt to create nil outbox event") o.logger.Warn("Attempt to create nil outbox event")
return merrors.InvalidArgument("outboxStore: nil outbox event") return merrors.InvalidArgument("outboxStore: nil outbox event")
} }
if err := o.repo.Insert(ctx, event, nil); err != nil { if err := o.repo.Insert(ctx, event, nil); err != nil {
if mongo.IsDuplicateKeyError(err) { if mongo.IsDuplicateKeyError(err) {
o.logger.Warn("duplicate event ID", zap.String("eventId", event.EventID)) o.logger.Warn("Duplicate event ID", zap.String("eventId", event.EventID))
return merrors.DataConflict("outbox event with this ID already exists") return merrors.DataConflict("outbox event with this ID already exists")
} }
o.logger.Warn("failed to create outbox event", zap.Error(err)) o.logger.Warn("Failed to create outbox event", zap.Error(err))
return err return err
} }
o.logger.Debug("outbox event created", zap.String("eventId", event.EventID), o.logger.Debug("Outbox event created", zap.String("eventId", event.EventID),
zap.String("subject", event.Subject)) zap.String("subject", event.Subject))
return nil return nil
} }
@@ -93,17 +93,17 @@ func (o *outboxStore) ListPending(ctx context.Context, limit int) ([]*model.Outb
return nil return nil
}) })
if err != nil { if err != nil {
o.logger.Warn("failed to list pending outbox events", zap.Error(err)) o.logger.Warn("Failed to list pending outbox events", zap.Error(err))
return nil, err return nil, err
} }
o.logger.Debug("listed pending outbox events", zap.Int("count", len(events))) o.logger.Debug("Listed pending outbox events", zap.Int("count", len(events)))
return events, nil return events, nil
} }
func (o *outboxStore) MarkSent(ctx context.Context, eventRef bson.ObjectID, sentAt time.Time) error { func (o *outboxStore) MarkSent(ctx context.Context, eventRef bson.ObjectID, sentAt time.Time) error {
if eventRef.IsZero() { if eventRef.IsZero() {
o.logger.Warn("attempt to mark sent with zero event ID") o.logger.Warn("Attempt to mark sent with zero event ID")
return merrors.InvalidArgument("outboxStore: zero event ID") return merrors.InvalidArgument("outboxStore: zero event ID")
} }
@@ -112,44 +112,44 @@ func (o *outboxStore) MarkSent(ctx context.Context, eventRef bson.ObjectID, sent
Set(repository.Field("sentAt"), sentAt) Set(repository.Field("sentAt"), sentAt)
if err := o.repo.Patch(ctx, eventRef, patch); err != nil { if err := o.repo.Patch(ctx, eventRef, patch); err != nil {
o.logger.Warn("failed to mark outbox event as sent", zap.Error(err), zap.String("eventRef", eventRef.Hex())) o.logger.Warn("Failed to mark outbox event as sent", zap.Error(err), zap.String("eventRef", eventRef.Hex()))
return err return err
} }
o.logger.Debug("outbox event marked as sent", zap.String("eventRef", eventRef.Hex())) o.logger.Debug("Outbox event marked as sent", zap.String("eventRef", eventRef.Hex()))
return nil return nil
} }
func (o *outboxStore) MarkFailed(ctx context.Context, eventRef bson.ObjectID) error { func (o *outboxStore) MarkFailed(ctx context.Context, eventRef bson.ObjectID) error {
if eventRef.IsZero() { if eventRef.IsZero() {
o.logger.Warn("attempt to mark failed with zero event ID") o.logger.Warn("Attempt to mark failed with zero event ID")
return merrors.InvalidArgument("outboxStore: zero event ID") return merrors.InvalidArgument("outboxStore: zero event ID")
} }
patch := repository.Patch().Set(repository.Field("status"), model.OutboxStatusFailed) patch := repository.Patch().Set(repository.Field("status"), model.OutboxStatusFailed)
if err := o.repo.Patch(ctx, eventRef, patch); err != nil { if err := o.repo.Patch(ctx, eventRef, patch); err != nil {
o.logger.Warn("failed to mark outbox event as failed", zap.Error(err), zap.String("eventRef", eventRef.Hex())) o.logger.Warn("Failed to mark outbox event as failed", zap.Error(err), zap.String("eventRef", eventRef.Hex()))
return err return err
} }
o.logger.Debug("outbox event marked as failed", zap.String("eventRef", eventRef.Hex())) o.logger.Debug("Outbox event marked as failed", zap.String("eventRef", eventRef.Hex()))
return nil return nil
} }
func (o *outboxStore) IncrementAttempts(ctx context.Context, eventRef bson.ObjectID) error { func (o *outboxStore) IncrementAttempts(ctx context.Context, eventRef bson.ObjectID) error {
if eventRef.IsZero() { if eventRef.IsZero() {
o.logger.Warn("attempt to increment attempts with zero event ID") o.logger.Warn("Attempt to increment attempts with zero event ID")
return merrors.InvalidArgument("outboxStore: zero event ID") return merrors.InvalidArgument("outboxStore: zero event ID")
} }
patch := repository.Patch().Inc(repository.Field("attempts"), 1) patch := repository.Patch().Inc(repository.Field("attempts"), 1)
if err := o.repo.Patch(ctx, eventRef, patch); err != nil { if err := o.repo.Patch(ctx, eventRef, patch); err != nil {
o.logger.Warn("failed to increment outbox attempts", zap.Error(err), zap.String("eventRef", eventRef.Hex())) o.logger.Warn("Failed to increment outbox attempts", zap.Error(err), zap.String("eventRef", eventRef.Hex()))
return err return err
} }
o.logger.Debug("outbox attempts incremented", zap.String("eventRef", eventRef.Hex())) o.logger.Debug("Outbox attempts incremented", zap.String("eventRef", eventRef.Hex()))
return nil return nil
} }

View File

@@ -31,7 +31,7 @@ func NewPostingLines(logger mlogger.Logger, db *mongo.Database) (storage.Posting
}, },
} }
if err := repo.CreateIndex(entryIndex); err != nil { if err := repo.CreateIndex(entryIndex); err != nil {
logger.Error("failed to ensure posting lines entry index", zap.Error(err)) logger.Error("Failed to ensure posting lines entry index", zap.Error(err))
return nil, err return nil, err
} }
@@ -43,12 +43,12 @@ func NewPostingLines(logger mlogger.Logger, db *mongo.Database) (storage.Posting
}, },
} }
if err := repo.CreateIndex(accountIndex); err != nil { if err := repo.CreateIndex(accountIndex); err != nil {
logger.Error("failed to ensure posting lines account index", zap.Error(err)) logger.Error("Failed to ensure posting lines account index", zap.Error(err))
return nil, err return nil, err
} }
childLogger := logger.Named(model.PostingLinesCollection) childLogger := logger.Named(model.PostingLinesCollection)
childLogger.Debug("posting lines store initialised", zap.String("collection", model.PostingLinesCollection)) childLogger.Debug("Posting lines store initialised", zap.String("collection", model.PostingLinesCollection))
return &postingLinesStore{ return &postingLinesStore{
logger: childLogger, logger: childLogger,
@@ -58,31 +58,31 @@ func NewPostingLines(logger mlogger.Logger, db *mongo.Database) (storage.Posting
func (p *postingLinesStore) CreateMany(ctx context.Context, lines []*model.PostingLine) error { func (p *postingLinesStore) CreateMany(ctx context.Context, lines []*model.PostingLine) error {
if len(lines) == 0 { if len(lines) == 0 {
p.logger.Warn("attempt to create empty posting lines array") p.logger.Warn("Attempt to create empty posting lines array")
return nil return nil
} }
storables := make([]storable.Storable, len(lines)) storables := make([]storable.Storable, len(lines))
for i, line := range lines { for i, line := range lines {
if line == nil { if line == nil {
p.logger.Warn("attempt to create nil posting line") p.logger.Warn("Attempt to create nil posting line")
return merrors.InvalidArgument("postingLinesStore: nil posting line") return merrors.InvalidArgument("postingLinesStore: nil posting line")
} }
storables[i] = line storables[i] = line
} }
if err := p.repo.InsertMany(ctx, storables); err != nil { if err := p.repo.InsertMany(ctx, storables); err != nil {
p.logger.Warn("failed to create posting lines", zap.Error(err), zap.Int("count", len(lines))) p.logger.Warn("Failed to create posting lines", zap.Error(err), zap.Int("count", len(lines)))
return err return err
} }
p.logger.Debug("posting lines created", zap.Int("count", len(lines))) p.logger.Debug("Posting lines created", zap.Int("count", len(lines)))
return nil return nil
} }
func (p *postingLinesStore) ListByJournalEntry(ctx context.Context, entryRef bson.ObjectID) ([]*model.PostingLine, error) { func (p *postingLinesStore) ListByJournalEntry(ctx context.Context, entryRef bson.ObjectID) ([]*model.PostingLine, error) {
if entryRef.IsZero() { if entryRef.IsZero() {
p.logger.Warn("attempt to list posting lines with zero entry ID") p.logger.Warn("Attempt to list posting lines with zero entry ID")
return nil, merrors.InvalidArgument("postingLinesStore: zero entry ID") return nil, merrors.InvalidArgument("postingLinesStore: zero entry ID")
} }
@@ -98,17 +98,17 @@ func (p *postingLinesStore) ListByJournalEntry(ctx context.Context, entryRef bso
return nil return nil
}) })
if err != nil { if err != nil {
p.logger.Warn("failed to list posting lines by entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef)) p.logger.Warn("Failed to list posting lines by entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef))
return nil, err return nil, err
} }
p.logger.Debug("listed posting lines by entry", zap.Int("count", len(lines)), mzap.ObjRef("entry_ref", entryRef)) p.logger.Debug("Listed posting lines by entry", zap.Int("count", len(lines)), mzap.ObjRef("entry_ref", entryRef))
return lines, nil return lines, nil
} }
func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef bson.ObjectID, limit int, offset int) ([]*model.PostingLine, error) { func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef bson.ObjectID, limit int, offset int) ([]*model.PostingLine, error) {
if accountRef.IsZero() { if accountRef.IsZero() {
p.logger.Warn("attempt to list posting lines with zero account ID") p.logger.Warn("Attempt to list posting lines with zero account ID")
return nil, merrors.InvalidArgument("postingLinesStore: zero account ID") return nil, merrors.InvalidArgument("postingLinesStore: zero account ID")
} }
@@ -130,10 +130,10 @@ func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef bson.O
return nil return nil
}) })
if err != nil { if err != nil {
p.logger.Warn("failed to list posting lines by account", zap.Error(err), mzap.AccRef(accountRef)) p.logger.Warn("Failed to list posting lines by account", zap.Error(err), mzap.AccRef(accountRef))
return nil, err return nil, err
} }
p.logger.Debug("listed posting lines by account", zap.Int("count", len(lines)), mzap.AccRef(accountRef)) p.logger.Debug("Listed posting lines by account", zap.Int("count", len(lines)), mzap.AccRef(accountRef))
return lines, nil return lines, nil
} }

View File

@@ -50,7 +50,7 @@ require (
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.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
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -225,8 +225,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -18,6 +18,7 @@ import (
type Client interface { type Client interface {
CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error)
GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error)
GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error)
UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error)
DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error) DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error)
SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error) SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error)
@@ -28,6 +29,7 @@ type Client interface {
type grpcPaymentMethodsClient interface { type grpcPaymentMethodsClient interface {
CreatePaymentMethod(ctx context.Context, in *methodsv1.CreatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.CreatePaymentMethodResponse, error) CreatePaymentMethod(ctx context.Context, in *methodsv1.CreatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.CreatePaymentMethodResponse, error)
GetPaymentMethod(ctx context.Context, in *methodsv1.GetPaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodResponse, error) GetPaymentMethod(ctx context.Context, in *methodsv1.GetPaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodResponse, error)
GetPaymentMethodPrivate(ctx context.Context, in *methodsv1.GetPaymentMethodPrivateRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodPrivateResponse, error)
UpdatePaymentMethod(ctx context.Context, in *methodsv1.UpdatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.UpdatePaymentMethodResponse, error) UpdatePaymentMethod(ctx context.Context, in *methodsv1.UpdatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.UpdatePaymentMethodResponse, error)
DeletePaymentMethod(ctx context.Context, in *methodsv1.DeletePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.DeletePaymentMethodResponse, error) DeletePaymentMethod(ctx context.Context, in *methodsv1.DeletePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.DeletePaymentMethodResponse, error)
SetPaymentMethodArchived(ctx context.Context, in *methodsv1.SetPaymentMethodArchivedRequest, opts ...grpc.CallOption) (*methodsv1.SetPaymentMethodArchivedResponse, error) SetPaymentMethodArchived(ctx context.Context, in *methodsv1.SetPaymentMethodArchivedRequest, opts ...grpc.CallOption) (*methodsv1.SetPaymentMethodArchivedResponse, error)
@@ -106,6 +108,12 @@ func (c *paymentMethodsClient) GetPaymentMethod(ctx context.Context, req *method
return c.client.GetPaymentMethod(callCtx, req) return c.client.GetPaymentMethod(callCtx, req)
} }
func (c *paymentMethodsClient) GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
callCtx, cancel := c.callContext(ctx)
defer cancel()
return c.client.GetPaymentMethodPrivate(callCtx, req)
}
func (c *paymentMethodsClient) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) { func (c *paymentMethodsClient) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) {
callCtx, cancel := c.callContext(ctx) callCtx, cancel := c.callContext(ctx)
defer cancel() defer cancel()

View File

@@ -48,5 +48,5 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
) )

View File

@@ -208,8 +208,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -51,7 +51,7 @@ func (i *Imp) Start() error {
permissionsDB := cfg.PermissionsDatabase permissionsDB := cfg.PermissionsDatabase
if permissionsDB == nil { if permissionsDB == nil {
i.logger.Info("permissions_database is not configured, falling back to database settings") i.logger.Info("Permissions_database is not configured, falling back to database settings")
permissionsDB = cfg.Database permissionsDB = cfg.Database
} }

View File

@@ -0,0 +1,168 @@
package methods
import (
"context"
"github.com/tech/sendico/pkg/merrors"
pkgmodel "github.com/tech/sendico/pkg/model"
methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
"go.mongodb.org/mongo-driver/v2/bson"
)
const (
paymentTypeAccount pkgmodel.PaymentType = 8
maxPrivateMethodResolutionDepth = 8
)
func (s *Service) GetPaymentMethodPrivate(ctx context.Context, req *methodsv1.GetPaymentMethodPrivateRequest) (*methodsv1.GetPaymentMethodPrivateResponse, error) {
if req == nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, merrors.InvalidArgument("request is required"))
}
if s.pmstore == nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, errStoreUnavailable)
}
if req.GetEndpoint() == methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_UNSPECIFIED {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, merrors.InvalidArgument("endpoint is required", "endpoint"))
}
organizationRef, err := parseObjectID(req.GetOrganizationRef(), "organization_ref")
if err != nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, err)
}
var resolved *pkgmodel.PaymentMethod
switch req.GetSelector().(type) {
case *methodsv1.GetPaymentMethodPrivateRequest_PaymentMethodRef:
methodRef, err := parseObjectID(req.GetPaymentMethodRef(), "payment_method_ref")
if err != nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, err)
}
resolved, err = s.resolvePrivateByMethodRef(ctx, organizationRef, methodRef, req.GetEndpoint(), 0)
if err != nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, err)
}
case *methodsv1.GetPaymentMethodPrivateRequest_PayeeRef:
payeeRef, err := parseObjectID(req.GetPayeeRef(), "payee_ref")
if err != nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, err)
}
resolved, err = s.resolvePrivateByRecipientRef(ctx, organizationRef, payeeRef, req.GetEndpoint(), 0)
if err != nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, err)
}
default:
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, merrors.InvalidArgument(
"selector must include payment_method_ref or payee_ref",
"selector",
))
}
record, err := encodePaymentMethodRecord(resolved)
if err != nil {
return autoError[methodsv1.GetPaymentMethodPrivateResponse](ctx, s.logger, err)
}
return &methodsv1.GetPaymentMethodPrivateResponse{
PaymentMethodRecord: record,
}, nil
}
func (s *Service) resolvePrivateByMethodRef(
ctx context.Context,
organizationRef bson.ObjectID,
methodRef bson.ObjectID,
endpoint methodsv1.PrivateEndpoint,
depth int,
) (*pkgmodel.PaymentMethod, error) {
method, err := s.pmstore.GetPrivate(ctx, methodRef)
if err != nil {
return nil, err
}
return s.resolvePrivateMethod(ctx, organizationRef, method, endpoint, depth)
}
func (s *Service) resolvePrivateByRecipientRef(
ctx context.Context,
organizationRef bson.ObjectID,
recipientRef bson.ObjectID,
endpoint methodsv1.PrivateEndpoint,
depth int,
) (*pkgmodel.PaymentMethod, error) {
items, err := s.pmstore.ListPrivate(ctx, organizationRef, recipientRef, nil)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, merrors.InvalidArgument("no payment methods available for recipient")
}
selected := pickPreferredPrivateMethod(items, endpoint)
if selected == nil {
return nil, merrors.InvalidArgument("no routable payment methods available for recipient")
}
return s.resolvePrivateMethod(ctx, organizationRef, selected, endpoint, depth)
}
func (s *Service) resolvePrivateMethod(
ctx context.Context,
organizationRef bson.ObjectID,
method *pkgmodel.PaymentMethod,
endpoint methodsv1.PrivateEndpoint,
depth int,
) (*pkgmodel.PaymentMethod, error) {
if method == nil {
return nil, merrors.InvalidArgument("payment method is required")
}
if depth >= maxPrivateMethodResolutionDepth {
return nil, merrors.InvalidArgument("payment method resolution depth exceeded")
}
if methodIsAccount(method) {
if method.RecipientRef.IsZero() {
return nil, merrors.InvalidArgument("account payment method recipient_ref is required")
}
return s.resolvePrivateByRecipientRef(ctx, organizationRef, method.RecipientRef, endpoint, depth+1)
}
return method, nil
}
func methodIsAccount(method *pkgmodel.PaymentMethod) bool {
if method == nil {
return false
}
return method.Type == paymentTypeAccount
}
func pickPreferredPrivateMethod(items []pkgmodel.PaymentMethod, endpoint methodsv1.PrivateEndpoint) *pkgmodel.PaymentMethod {
switch endpoint {
case methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_SOURCE:
return pickMainThenAnyNonAccount(items)
case methodsv1.PrivateEndpoint_PRIVATE_ENDPOINT_DESTINATION:
return pickMainThenAnyNonAccount(items)
default:
return nil
}
}
func pickMainThenAnyNonAccount(items []pkgmodel.PaymentMethod) *pkgmodel.PaymentMethod {
for i := range items {
if items[i].IsMain && !methodIsAccount(&items[i]) {
return &items[i]
}
}
for i := range items {
if !methodIsAccount(&items[i]) {
return &items[i]
}
}
for i := range items {
if items[i].IsMain {
return &items[i]
}
}
if len(items) == 0 {
return nil
}
return &items[0]
}

View File

@@ -64,5 +64,5 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.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
) )

View File

@@ -211,8 +211,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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
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/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 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -49,7 +49,7 @@ func stepLiveness(
pStep, ok := pStepIdx[step.Code] pStep, ok := pStepIdx[step.Code]
if !ok { if !ok {
logger.Error("step missing in payment plan", logger.Error("Step missing in payment plan",
zap.String("step_id", step.Code), zap.String("step_id", step.Code),
) )
return StepDead return StepDead
@@ -58,7 +58,7 @@ func stepLiveness(
for _, depID := range pStep.DependsOn { for _, depID := range pStep.DependsOn {
dep := eStepIdx[depID] dep := eStepIdx[depID]
if dep == nil { if dep == nil {
logger.Warn("dependency missing in execution plan", logger.Warn("Dependency missing in execution plan",
zap.String("step_id", step.Code), zap.String("step_id", step.Code),
zap.String("dep_id", depID), zap.String("dep_id", depID),
) )

Some files were not shown because too many files have changed in this diff Show More