outbox for gateways
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
if !isFinalTransferStatus(status) {
|
||||||
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
||||||
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))
|
||||||
}
|
}
|
||||||
if isFinalStatus(transfer) {
|
|
||||||
s.emitTransferStatusEvent(transfer)
|
|
||||||
}
|
|
||||||
return transfer, 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 {
|
||||||
|
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer, _ := res.(*model.Transfer)
|
||||||
|
return transfer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -18,10 +20,12 @@ 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.
|
||||||
@@ -38,6 +42,7 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
|
|||||||
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)
|
||||||
|
|||||||
38
api/gateway/chain/storage/mongo/transaction.go
Normal file
38
api/gateway/chain/storage/mongo/transaction.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mongoTransactionFactory struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction {
|
||||||
|
return &mongoTransaction{client: f.client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mongoTransaction struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||||
|
session, err := t.client.StartSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer session.EndSession(ctx)
|
||||||
|
|
||||||
|
run := func(sessCtx context.Context) (any, error) {
|
||||||
|
return cb(sessCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.WithTransaction(ctx, run)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMongoTransactionFactory(client *mongo.Client) transaction.Factory {
|
||||||
|
return &mongoTransactionFactory{client: client}
|
||||||
|
}
|
||||||
30
api/gateway/common/go.mod
Normal file
30
api/gateway/common/go.mod
Normal 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
158
api/gateway/common/go.sum
Normal 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=
|
||||||
33
api/gateway/common/outbox/model.go
Normal file
33
api/gateway/common/outbox/model.go
Normal 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
|
||||||
|
}
|
||||||
123
api/gateway/common/outbox/mongo_store.go
Normal file
123
api/gateway/common/outbox/mongo_store.go
Normal 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)
|
||||||
|
}
|
||||||
108
api/gateway/common/outbox/reliable_adapter.go
Normal file
108
api/gateway/common/outbox/reliable_adapter.go
Normal 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)
|
||||||
|
}
|
||||||
330
api/gateway/common/outbox/reliable_adapter_integration_test.go
Normal file
330
api/gateway/common/outbox/reliable_adapter_integration_test.go
Normal 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 ©Event
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
72
api/gateway/common/outbox/reliable_runtime.go
Normal file
72
api/gateway/common/outbox/reliable_runtime.go
Normal 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()
|
||||||
|
}
|
||||||
17
api/gateway/common/outbox/store.go
Normal file
17
api/gateway/common/outbox/store.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
50
api/gateway/mntx/internal/service/gateway/outbox_reliable.go
Normal file
50
api/gateway/mntx/internal/service/gateway/outbox_reliable.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
gatewayoutbox "github.com/tech/sendico/gateway/common/outbox"
|
||||||
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
me "github.com/tech/sendico/pkg/messaging/envelope"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mntxOutboxProvider interface {
|
||||||
|
Outbox() gatewayoutbox.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
type mntxTransactionProvider interface {
|
||||||
|
TransactionFactory() transaction.Factory
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cardPayoutProcessor) outboxStore() gatewayoutbox.Store {
|
||||||
|
provider, ok := p.store.(mntxOutboxProvider)
|
||||||
|
if !ok || provider == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return provider.Outbox()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cardPayoutProcessor) startOutboxReliableProducer() error {
|
||||||
|
if p == nil || p.outbox == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.outbox.Start(p.logger, p.producer, p.outboxStore(), p.msgCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cardPayoutProcessor) sendWithOutbox(ctx context.Context, env me.Envelope) error {
|
||||||
|
if err := p.startOutboxReliableProducer(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.outbox == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.outbox.Send(ctx, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cardPayoutProcessor) executeTransaction(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||||
|
provider, ok := p.store.(mntxTransactionProvider)
|
||||||
|
if !ok || provider == nil || provider.TransactionFactory() == nil {
|
||||||
|
return cb(ctx)
|
||||||
|
}
|
||||||
|
return provider.TransactionFactory().CreateTransaction().Execute(ctx, cb)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 !isFinalStatus(state) {
|
||||||
if err := p.store.Payouts().Upsert(ctx, state); err != nil {
|
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),
|
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
|
||||||
|
}
|
||||||
|
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 isFinalStatus(state) {
|
||||||
p.emitTransferStatusEvent(state)
|
if emitErr := p.emitTransferStatusEvent(txCtx, state); emitErr != nil {
|
||||||
|
return nil, emitErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID),
|
||||||
|
zap.String("payment_ref", state.PaymentRef), zap.String("status", string(state.Status)),
|
||||||
|
)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -17,8 +19,10 @@ 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) {
|
||||||
@@ -45,6 +49,7 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
|||||||
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)
|
||||||
|
|||||||
38
api/gateway/mntx/storage/mongo/transaction.go
Normal file
38
api/gateway/mntx/storage/mongo/transaction.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mongoTransactionFactory struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction {
|
||||||
|
return &mongoTransaction{client: f.client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mongoTransaction struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||||
|
session, err := t.client.StartSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer session.EndSession(ctx)
|
||||||
|
|
||||||
|
run := func(sessCtx context.Context) (any, error) {
|
||||||
|
return cb(sessCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.WithTransaction(ctx, run)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMongoTransactionFactory(client *mongo.Client) transaction.Factory {
|
||||||
|
return &mongoTransactionFactory{client: client}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ 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,
|
||||||
@@ -97,6 +101,7 @@ func (i *Imp) Start() error {
|
|||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -54,6 +56,7 @@ type Config struct {
|
|||||||
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 !isFinalStatus(record) {
|
||||||
if err := s.repo.Payments().Upsert(ctx, record); err != nil {
|
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))
|
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
|
||||||
}
|
}
|
||||||
|
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 isFinalStatus(record) {
|
||||||
s.emitTransferStatusEvent(ctx, 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))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -17,9 +19,11 @@ 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) {
|
||||||
@@ -46,6 +50,7 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Repository, error) {
|
|||||||
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)
|
||||||
|
|||||||
38
api/gateway/tgsettle/storage/mongo/transaction.go
Normal file
38
api/gateway/tgsettle/storage/mongo/transaction.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mongoTransactionFactory struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction {
|
||||||
|
return &mongoTransaction{client: f.client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mongoTransaction struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||||
|
session, err := t.client.StartSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer session.EndSession(ctx)
|
||||||
|
|
||||||
|
run := func(sessCtx context.Context) (any, error) {
|
||||||
|
return cb(sessCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.WithTransaction(ctx, run)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMongoTransactionFactory(client *mongo.Client) transaction.Factory {
|
||||||
|
return &mongoTransactionFactory{client: client}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
47
api/gateway/tron/internal/service/gateway/outbox_reliable.go
Normal file
47
api/gateway/tron/internal/service/gateway/outbox_reliable.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
if !isFinalTransferStatus(status) {
|
||||||
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
transfer, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, status, failureReason, txHash)
|
||||||
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))
|
||||||
}
|
}
|
||||||
if isFinalStatus(transfer) {
|
|
||||||
s.emitTransferStatusEvent(transfer)
|
|
||||||
}
|
|
||||||
return transfer, 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 {
|
||||||
|
s.logger.Warn("Failed to update transfer status", zap.String("transfer_ref", transferRef), zap.String("status", string(status)), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer, _ := res.(*model.Transfer)
|
||||||
|
return transfer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -18,10 +20,12 @@ 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.
|
||||||
@@ -38,6 +42,7 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
|
|||||||
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)
|
||||||
|
|||||||
38
api/gateway/tron/storage/mongo/transaction.go
Normal file
38
api/gateway/tron/storage/mongo/transaction.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/db/transaction"
|
||||||
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mongoTransactionFactory struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction {
|
||||||
|
return &mongoTransaction{client: f.client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mongoTransaction struct {
|
||||||
|
client *mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||||
|
session, err := t.client.StartSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer session.EndSession(ctx)
|
||||||
|
|
||||||
|
run := func(sessCtx context.Context) (any, error) {
|
||||||
|
return cb(sessCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.WithTransaction(ctx, run)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMongoTransactionFactory(client *mongo.Client) transaction.Factory {
|
||||||
|
return &mongoTransactionFactory{client: client}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
168
api/ledger/internal/service/ledger/outbox_reliable.go
Normal file
168
api/ledger/internal/service/ledger/outbox_reliable.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +45,7 @@ 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
|
||||||
@@ -51,7 +53,7 @@ type Service struct {
|
|||||||
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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
168
api/payments/methods/internal/service/methods/get_private.go
Normal file
168
api/payments/methods/internal/service/methods/get_private.go
Normal 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]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user