unified gateway interfaces
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -75,7 +76,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
||||
return &chainGatewayClient{
|
||||
cfg: cfg,
|
||||
conn: conn,
|
||||
client: chainv1.NewChainGatewayServiceClient(conn),
|
||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260104020744-7268a54d0358 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // 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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f h1:a5PUgHGinaD6XrLmIDLQmGHocjIjBsBAcR5gALjZvMU=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260104020744-7268a54d0358 h1:B6uGMdZ4maUTJm+LYgBwEIDuJxgOUACw8K0Yg6jpNbY=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260104020744-7268a54d0358/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -33,7 +34,7 @@ var (
|
||||
errStorageUnavailable = serviceError("chain_gateway: storage not initialised")
|
||||
)
|
||||
|
||||
// Service implements the ChainGatewayService RPC contract.
|
||||
// Service implements the UnifiedGatewayService RPC contract for chain operations.
|
||||
type Service struct {
|
||||
logger mlogger.Logger
|
||||
storage storage.Repository
|
||||
@@ -51,7 +52,7 @@ type Service struct {
|
||||
commands commands.Registry
|
||||
announcers []*discovery.Announcer
|
||||
|
||||
chainv1.UnimplementedChainGatewayServiceServer
|
||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
||||
}
|
||||
|
||||
// NewService constructs the chain gateway service skeleton.
|
||||
@@ -94,7 +95,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
// Register wires the service onto the provided gRPC router.
|
||||
func (s *Service) Register(router routers.GRPC) error {
|
||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||
chainv1.RegisterChainGatewayServiceServer(reg, s)
|
||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,6 +209,7 @@ func (s *Service) startDiscoveryAnnouncers() {
|
||||
Network: network.Name,
|
||||
Operations: []string{"balance.read", "payin.crypto", "payout.crypto", "fee.send"},
|
||||
Currencies: currencies,
|
||||
InvokeURI: discovery.DefaultInvokeURI(string(mservice.ChainGateway)),
|
||||
Version: version,
|
||||
}
|
||||
announcer := discovery.NewAnnouncer(s.logger, s.producer, string(mservice.ChainGateway), announce)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -23,7 +24,7 @@ type Client interface {
|
||||
|
||||
type gatewayClient struct {
|
||||
conn *grpc.ClientConn
|
||||
client mntxv1.MntxGatewayServiceClient
|
||||
client unifiedv1.UnifiedGatewayServiceClient
|
||||
cfg Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
@@ -48,7 +49,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
||||
|
||||
return &gatewayClient{
|
||||
conn: conn,
|
||||
client: mntxv1.NewMntxGatewayServiceClient(conn),
|
||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
||||
cfg: cfg,
|
||||
logger: cfg.Logger,
|
||||
}, nil
|
||||
|
||||
@@ -7,7 +7,6 @@ replace github.com/tech/sendico/pkg => ../../pkg
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
go.uber.org/zap v1.27.1
|
||||
google.golang.org/grpc v1.78.0
|
||||
@@ -22,8 +21,8 @@ require (
|
||||
github.com/casbin/govaluate v1.10.0 // indirect
|
||||
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
@@ -40,8 +40,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
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/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=
|
||||
@@ -59,6 +57,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
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.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -125,8 +125,6 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i
|
||||
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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@@ -31,7 +32,7 @@ type Service struct {
|
||||
gatewayDescriptor *gatewayv1.GatewayInstanceDescriptor
|
||||
announcer *discovery.Announcer
|
||||
|
||||
mntxv1.UnimplementedMntxGatewayServiceServer
|
||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
||||
}
|
||||
|
||||
type payoutFailure interface {
|
||||
@@ -96,7 +97,7 @@ func NewService(logger mlogger.Logger, opts ...Option) *Service {
|
||||
// Register wires the service onto the provided gRPC router.
|
||||
func (s *Service) Register(router routers.GRPC) error {
|
||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||
mntxv1.RegisterMntxGatewayServiceServer(reg, s)
|
||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,6 +146,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
Service: "CARD_PAYOUT_RAIL_GATEWAY",
|
||||
Rail: "CARD_PAYOUT",
|
||||
Operations: []string{"payout.card"},
|
||||
InvokeURI: discovery.DefaultInvokeURI(string(mservice.MntxGateway)),
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
if s.gatewayDescriptor != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ messaging:
|
||||
reconnect_wait: 5
|
||||
|
||||
gateway:
|
||||
rail: "card"
|
||||
rail: "provider_settlement"
|
||||
target_chat_id_env: TGSETTLE_GATEWAY_CHAT_ID
|
||||
timeout_seconds: 120
|
||||
accepted_user_ids: []
|
||||
|
||||
@@ -8,6 +8,8 @@ require (
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
go.mongodb.org/mongo-driver v1.17.6
|
||||
go.uber.org/zap v1.27.1
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -46,6 +48,4 @@ require (
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
@@ -20,8 +20,16 @@ import (
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
"github.com/tech/sendico/pkg/server/grpcapp"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,6 +37,13 @@ const (
|
||||
executedStatus = "executed"
|
||||
)
|
||||
|
||||
const (
|
||||
metadataPaymentIntentID = "payment_intent_id"
|
||||
metadataQuoteRef = "quote_ref"
|
||||
metadataTargetChatID = "target_chat_id"
|
||||
metadataOutgoingLeg = "outgoing_leg"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Rail string
|
||||
TargetChatIDEnv string
|
||||
@@ -49,6 +64,8 @@ type Service struct {
|
||||
mu sync.Mutex
|
||||
pending map[string]*model.PaymentGatewayIntent
|
||||
consumers []msg.Consumer
|
||||
|
||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
||||
}
|
||||
|
||||
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, broker mb.Broker, cfg Config) *Service {
|
||||
@@ -56,13 +73,13 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
logger = logger.Named("tgsettle_gateway")
|
||||
}
|
||||
svc := &Service{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
producer: producer,
|
||||
broker: broker,
|
||||
cfg: cfg,
|
||||
rail: strings.TrimSpace(cfg.Rail),
|
||||
pending: map[string]*model.PaymentGatewayIntent{},
|
||||
broker: broker,
|
||||
cfg: cfg,
|
||||
rail: strings.TrimSpace(cfg.Rail),
|
||||
pending: map[string]*model.PaymentGatewayIntent{},
|
||||
}
|
||||
svc.chatID = strings.TrimSpace(readEnv(cfg.TargetChatIDEnv))
|
||||
svc.startConsumers()
|
||||
@@ -70,8 +87,10 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *Service) Register(_ routers.GRPC) error {
|
||||
return nil
|
||||
func (s *Service) Register(router routers.GRPC) error {
|
||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Shutdown() {
|
||||
@@ -95,8 +114,6 @@ func (s *Service) startConsumers() {
|
||||
}
|
||||
return
|
||||
}
|
||||
intentProcessor := paymentgateway.NewPaymentGatewayIntentProcessor(s.logger, s.onIntent)
|
||||
s.consumeProcessor(intentProcessor)
|
||||
resultProcessor := confirmations.NewConfirmationResultProcessor(s.logger, string(mservice.PaymentGateway), s.rail, s.onConfirmationResult)
|
||||
s.consumeProcessor(resultProcessor)
|
||||
}
|
||||
@@ -115,6 +132,62 @@ func (s *Service) consumeProcessor(processor np.EnvelopeProcessor) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) SubmitTransfer(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: request is required")
|
||||
}
|
||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||
if idempotencyKey == "" {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: idempotency_key is required")
|
||||
}
|
||||
amount := req.GetAmount()
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: amount is required")
|
||||
}
|
||||
intent, err := intentFromSubmitTransfer(req, s.rail, s.chatID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.repo == nil || s.repo.Payments() == nil {
|
||||
return nil, merrors.Internal("payment gateway storage unavailable")
|
||||
}
|
||||
existing, err := s.repo.Payments().FindByIdempotencyKey(ctx, idempotencyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
return &chainv1.SubmitTransferResponse{Transfer: transferFromExecution(existing, req)}, nil
|
||||
}
|
||||
if err := s.onIntent(ctx, intent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chainv1.SubmitTransferResponse{Transfer: transferFromRequest(req)}, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTransfer(ctx context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("get_transfer: request is required")
|
||||
}
|
||||
transferRef := strings.TrimSpace(req.GetTransferRef())
|
||||
if transferRef == "" {
|
||||
return nil, merrors.InvalidArgument("get_transfer: transfer_ref is required")
|
||||
}
|
||||
if s.repo == nil || s.repo.Payments() == nil {
|
||||
return nil, merrors.Internal("payment gateway storage unavailable")
|
||||
}
|
||||
existing, err := s.repo.Payments().FindByIdempotencyKey(ctx, transferRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
return &chainv1.GetTransferResponse{Transfer: transferFromExecution(existing, nil)}, nil
|
||||
}
|
||||
if s.hasPending(transferRef) {
|
||||
return &chainv1.GetTransferResponse{Transfer: transferPending(transferRef)}, nil
|
||||
}
|
||||
return nil, status.Error(codes.NotFound, "transfer not found")
|
||||
}
|
||||
|
||||
func (s *Service) onIntent(ctx context.Context, intent *model.PaymentGatewayIntent) error {
|
||||
if intent == nil {
|
||||
return merrors.InvalidArgument("payment gateway intent is nil", "intent")
|
||||
@@ -178,11 +251,11 @@ func (s *Service) onConfirmationResult(ctx context.Context, result *model.Confir
|
||||
|
||||
if result.Status == model.ConfirmationStatusConfirmed || result.Status == model.ConfirmationStatusClarified {
|
||||
exec := &storagemodel.PaymentExecution{
|
||||
IdempotencyKey: intent.IdempotencyKey,
|
||||
IdempotencyKey: intent.IdempotencyKey,
|
||||
PaymentIntentID: intent.PaymentIntentID,
|
||||
ExecutedMoney: result.Money,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
Status: executedStatus,
|
||||
ExecutedMoney: result.Money,
|
||||
QuoteRef: intent.QuoteRef,
|
||||
Status: executedStatus,
|
||||
}
|
||||
if err := s.repo.Payments().InsertExecution(ctx, exec); err != nil && err != storage.ErrDuplicate {
|
||||
return err
|
||||
@@ -290,11 +363,22 @@ func (s *Service) removeIntent(requestID string) {
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Service) hasPending(requestID string) bool {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" {
|
||||
return false
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
_, ok := s.pending[requestID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *Service) startAnnouncer() {
|
||||
if s == nil || s.producer == nil {
|
||||
return
|
||||
}
|
||||
caps := []string{"telegram_confirmation", "money_persistence"}
|
||||
caps := []string{"telegram_confirmation", "money_persistence", "observe.confirm", "payout.fiat"}
|
||||
if s.rail != "" {
|
||||
caps = append(caps, "confirmations."+strings.ToLower(string(mservice.PaymentGateway))+"."+strings.ToLower(s.rail))
|
||||
}
|
||||
@@ -302,6 +386,7 @@ func (s *Service) startAnnouncer() {
|
||||
Service: string(mservice.PaymentGateway),
|
||||
Rail: s.rail,
|
||||
Operations: caps,
|
||||
InvokeURI: discovery.DefaultInvokeURI(string(mservice.PaymentGateway)),
|
||||
}
|
||||
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.PaymentGateway), announce)
|
||||
s.announcer.Start()
|
||||
@@ -324,6 +409,128 @@ func normalizeIntent(intent *model.PaymentGatewayIntent) *model.PaymentGatewayIn
|
||||
return &cp
|
||||
}
|
||||
|
||||
func intentFromSubmitTransfer(req *chainv1.SubmitTransferRequest, defaultRail, defaultChatID string) (*model.PaymentGatewayIntent, error) {
|
||||
if req == nil {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: request is required")
|
||||
}
|
||||
idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey())
|
||||
if idempotencyKey == "" {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: idempotency_key is required")
|
||||
}
|
||||
amount := req.GetAmount()
|
||||
if amount == nil {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: amount is required")
|
||||
}
|
||||
requestedMoney := &paymenttypes.Money{
|
||||
Amount: strings.TrimSpace(amount.GetAmount()),
|
||||
Currency: strings.TrimSpace(amount.GetCurrency()),
|
||||
}
|
||||
if requestedMoney.Amount == "" || requestedMoney.Currency == "" {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: amount is required")
|
||||
}
|
||||
metadata := req.GetMetadata()
|
||||
paymentIntentID := strings.TrimSpace(req.GetClientReference())
|
||||
if paymentIntentID == "" {
|
||||
paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
|
||||
}
|
||||
if paymentIntentID == "" {
|
||||
return nil, merrors.InvalidArgument("submit_transfer: payment_intent_id is required")
|
||||
}
|
||||
quoteRef := strings.TrimSpace(metadata[metadataQuoteRef])
|
||||
targetChatID := strings.TrimSpace(metadata[metadataTargetChatID])
|
||||
outgoingLeg := strings.TrimSpace(metadata[metadataOutgoingLeg])
|
||||
if outgoingLeg == "" {
|
||||
outgoingLeg = strings.TrimSpace(defaultRail)
|
||||
}
|
||||
if targetChatID == "" {
|
||||
targetChatID = strings.TrimSpace(defaultChatID)
|
||||
}
|
||||
return &model.PaymentGatewayIntent{
|
||||
PaymentIntentID: paymentIntentID,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OutgoingLeg: outgoingLeg,
|
||||
QuoteRef: quoteRef,
|
||||
RequestedMoney: requestedMoney,
|
||||
TargetChatID: targetChatID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func transferFromRequest(req *chainv1.SubmitTransferRequest) *chainv1.Transfer {
|
||||
if req == nil {
|
||||
return nil
|
||||
}
|
||||
amount := req.GetAmount()
|
||||
return &chainv1.Transfer{
|
||||
TransferRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
|
||||
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
|
||||
SourceWalletRef: strings.TrimSpace(req.GetSourceWalletRef()),
|
||||
Destination: req.GetDestination(),
|
||||
RequestedAmount: amount,
|
||||
Status: chainv1.TransferStatus_TRANSFER_SUBMITTED,
|
||||
}
|
||||
}
|
||||
|
||||
func transferFromExecution(exec *storagemodel.PaymentExecution, req *chainv1.SubmitTransferRequest) *chainv1.Transfer {
|
||||
if exec == nil {
|
||||
return nil
|
||||
}
|
||||
var requested *moneyv1.Money
|
||||
if req != nil && req.GetAmount() != nil {
|
||||
requested = req.GetAmount()
|
||||
}
|
||||
net := moneyFromPayment(exec.ExecutedMoney)
|
||||
status := chainv1.TransferStatus_TRANSFER_CONFIRMED
|
||||
if strings.TrimSpace(exec.Status) != "" && !strings.EqualFold(exec.Status, executedStatus) {
|
||||
status = chainv1.TransferStatus_TRANSFER_PENDING
|
||||
}
|
||||
transfer := &chainv1.Transfer{
|
||||
TransferRef: strings.TrimSpace(exec.IdempotencyKey),
|
||||
IdempotencyKey: strings.TrimSpace(exec.IdempotencyKey),
|
||||
RequestedAmount: requested,
|
||||
NetAmount: net,
|
||||
Status: status,
|
||||
}
|
||||
if req != nil {
|
||||
transfer.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef())
|
||||
transfer.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef())
|
||||
transfer.Destination = req.GetDestination()
|
||||
}
|
||||
if !exec.ExecutedAt.IsZero() {
|
||||
ts := timestamppb.New(exec.ExecutedAt)
|
||||
transfer.CreatedAt = ts
|
||||
transfer.UpdatedAt = ts
|
||||
}
|
||||
return transfer
|
||||
}
|
||||
|
||||
func transferPending(requestID string) *chainv1.Transfer {
|
||||
ref := strings.TrimSpace(requestID)
|
||||
if ref == "" {
|
||||
return nil
|
||||
}
|
||||
return &chainv1.Transfer{
|
||||
TransferRef: ref,
|
||||
IdempotencyKey: ref,
|
||||
Status: chainv1.TransferStatus_TRANSFER_SUBMITTED,
|
||||
}
|
||||
}
|
||||
|
||||
func moneyFromPayment(m *paymenttypes.Money) *moneyv1.Money {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
currency := strings.TrimSpace(m.Currency)
|
||||
amount := strings.TrimSpace(m.Amount)
|
||||
if currency == "" || amount == "" {
|
||||
return nil
|
||||
}
|
||||
return &moneyv1.Money{
|
||||
Currency: currency,
|
||||
Amount: amount,
|
||||
}
|
||||
}
|
||||
|
||||
func readEnv(env string) string {
|
||||
if strings.TrimSpace(env) == "" {
|
||||
return ""
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/payments/rail"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
@@ -81,7 +82,7 @@ func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, erro
|
||||
return &ledgerClient{
|
||||
cfg: cfg,
|
||||
conn: conn,
|
||||
client: ledgerv1.NewLedgerServiceClient(conn),
|
||||
client: unifiedv1.NewUnifiedGatewayServiceClient(conn),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
pmessaging "github.com/tech/sendico/pkg/messaging"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
unifiedv1 "github.com/tech/sendico/pkg/proto/gateway/unified/v1"
|
||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||
)
|
||||
|
||||
@@ -49,7 +50,7 @@ type Service struct {
|
||||
cancel context.CancelFunc
|
||||
publisher *outboxPublisher
|
||||
}
|
||||
ledgerv1.UnimplementedLedgerServiceServer
|
||||
unifiedv1.UnimplementedUnifiedGatewayServiceServer
|
||||
}
|
||||
|
||||
type feesDependency struct {
|
||||
@@ -82,7 +83,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.
|
||||
|
||||
func (s *Service) Register(router routers.GRPC) error {
|
||||
return router.Register(func(reg grpc.ServiceRegistrar) {
|
||||
ledgerv1.RegisterLedgerServiceServer(reg, s)
|
||||
unifiedv1.RegisterUnifiedGatewayServiceServer(reg, s)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,6 +205,7 @@ func (s *Service) startDiscoveryAnnouncer() {
|
||||
announce := discovery.Announcement{
|
||||
Service: "LEDGER",
|
||||
Operations: []string{"balance.read", "ledger.debit", "ledger.credit"},
|
||||
InvokeURI: discovery.DefaultInvokeURI(string(mservice.Ledger)),
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.Ledger), announce)
|
||||
|
||||
@@ -8,7 +8,7 @@ require (
|
||||
github.com/amplitude/analytics-go v1.3.0
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/sendgrid/sendgrid-go v3.16.1+incompatible
|
||||
github.com/tech/sendico/pkg v0.1.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
|
||||
@@ -2,8 +2,8 @@ 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/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/amplitude/analytics-go v1.3.0 h1:Lgj31fWThQ6hdDHO0RPxQfy/D7d8K+aqWsBa+IGTxQk=
|
||||
@@ -105,8 +105,8 @@ github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||
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/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
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=
|
||||
@@ -189,6 +189,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
|
||||
@@ -51,6 +51,12 @@ gateway:
|
||||
call_timeout_seconds: 3
|
||||
insecure: true
|
||||
|
||||
payment_gateway:
|
||||
address: "sendico_tgsettle_gateway:50080"
|
||||
dial_timeout_seconds: 5
|
||||
call_timeout_seconds: 3
|
||||
insecure: true
|
||||
|
||||
mntx:
|
||||
address: "sendico_mntx_gateway:50075"
|
||||
dial_timeout_seconds: 5
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"strings"
|
||||
|
||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/discovery"
|
||||
@@ -48,15 +47,15 @@ func buildFeeLedgerAccounts(src map[string]string) map[string]string {
|
||||
return result
|
||||
}
|
||||
|
||||
func buildGatewayRegistry(logger mlogger.Logger, mntxClient mntxclient.Client, src []gatewayInstanceConfig, registry *discovery.Registry) orchestrator.GatewayRegistry {
|
||||
func buildGatewayRegistry(logger mlogger.Logger, src []gatewayInstanceConfig, registry *discovery.Registry) orchestrator.GatewayRegistry {
|
||||
static := buildGatewayInstances(logger, src)
|
||||
staticRegistry := orchestrator.NewGatewayRegistry(logger, mntxClient, static)
|
||||
staticRegistry := orchestrator.NewGatewayRegistry(logger, static)
|
||||
discoveryRegistry := orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
||||
return orchestrator.NewCompositeGatewayRegistry(logger, staticRegistry, discoveryRegistry)
|
||||
}
|
||||
|
||||
func buildRailGateways(chainClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
|
||||
if chainClient == nil || len(src) == 0 {
|
||||
func buildRailGateways(chainClient chainclient.Client, paymentGatewayClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
|
||||
if len(src) == 0 || (chainClient == nil && paymentGatewayClient == nil) {
|
||||
return nil
|
||||
}
|
||||
instances := buildGatewayInstances(nil, src)
|
||||
@@ -68,9 +67,6 @@ func buildRailGateways(chainClient chainclient.Client, src []gatewayInstanceConf
|
||||
if inst == nil || !inst.IsEnabled {
|
||||
continue
|
||||
}
|
||||
if inst.Rail != model.RailCrypto {
|
||||
continue
|
||||
}
|
||||
cfg := chainclient.RailGatewayConfig{
|
||||
Rail: string(inst.Rail),
|
||||
Network: inst.Network,
|
||||
@@ -82,7 +78,18 @@ func buildRailGateways(chainClient chainclient.Client, src []gatewayInstanceConf
|
||||
RequiresObserveConfirm: inst.Capabilities.RequiresObserveConfirm,
|
||||
},
|
||||
}
|
||||
result[inst.ID] = chainclient.NewRailGateway(chainClient, cfg)
|
||||
switch inst.Rail {
|
||||
case model.RailCrypto:
|
||||
if chainClient == nil {
|
||||
continue
|
||||
}
|
||||
result[inst.ID] = chainclient.NewRailGateway(chainClient, cfg)
|
||||
case model.RailProviderSettlement:
|
||||
if paymentGatewayClient == nil {
|
||||
continue
|
||||
}
|
||||
result[inst.ID] = orchestrator.NewProviderSettlementGateway(paymentGatewayClient, cfg)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -85,6 +85,29 @@ func (i *Imp) initGatewayClient(cfg clientConfig) chainclient.Client {
|
||||
return client
|
||||
}
|
||||
|
||||
func (i *Imp) initPaymentGatewayClient(cfg clientConfig) chainclient.Client {
|
||||
addr := cfg.address()
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
||||
defer cancel()
|
||||
|
||||
client, err := chainclient.New(ctx, chainclient.Config{
|
||||
Address: addr,
|
||||
DialTimeout: cfg.dialTimeout(),
|
||||
CallTimeout: cfg.callTimeout(),
|
||||
Insecure: cfg.InsecureTransport,
|
||||
})
|
||||
if err != nil {
|
||||
i.logger.Warn("failed to connect to payment gateway service", zap.String("address", addr), zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
i.logger.Info("connected to payment gateway service", zap.String("address", addr))
|
||||
return client
|
||||
}
|
||||
|
||||
func (i *Imp) initMntxClient(cfg clientConfig) mntxclient.Client {
|
||||
addr := cfg.address()
|
||||
if addr == "" {
|
||||
@@ -138,6 +161,9 @@ func (i *Imp) closeClients() {
|
||||
if i.gatewayClient != nil {
|
||||
_ = i.gatewayClient.Close()
|
||||
}
|
||||
if i.paymentGatewayClient != nil {
|
||||
_ = i.paymentGatewayClient.Close()
|
||||
}
|
||||
if i.mntxClient != nil {
|
||||
_ = i.mntxClient.Close()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type config struct {
|
||||
Fees clientConfig `yaml:"fees"`
|
||||
Ledger clientConfig `yaml:"ledger"`
|
||||
Gateway clientConfig `yaml:"gateway"`
|
||||
PaymentGateway clientConfig `yaml:"payment_gateway"`
|
||||
Mntx clientConfig `yaml:"mntx"`
|
||||
Oracle clientConfig `yaml:"oracle"`
|
||||
CardGateways map[string]cardGatewayRouteConfig `yaml:"card_gateways"`
|
||||
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
)
|
||||
|
||||
type orchestratorDeps struct {
|
||||
feesClient feesv1.FeeEngineClient
|
||||
ledgerClient ledgerclient.Client
|
||||
gatewayClient chainclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
feesClient feesv1.FeeEngineClient
|
||||
ledgerClient ledgerclient.Client
|
||||
gatewayClient chainclient.Client
|
||||
paymentGatewayClient chainclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
}
|
||||
|
||||
func (i *Imp) initDependencies(cfg *config) *orchestratorDeps {
|
||||
@@ -35,6 +36,11 @@ func (i *Imp) initDependencies(cfg *config) *orchestratorDeps {
|
||||
i.gatewayClient = deps.gatewayClient
|
||||
}
|
||||
|
||||
deps.paymentGatewayClient = i.initPaymentGatewayClient(cfg.PaymentGateway)
|
||||
if deps.paymentGatewayClient != nil {
|
||||
i.paymentGatewayClient = deps.paymentGatewayClient
|
||||
}
|
||||
|
||||
deps.mntxClient = i.initMntxClient(cfg.Mntx)
|
||||
if deps.mntxClient != nil {
|
||||
i.mntxClient = deps.mntxClient
|
||||
@@ -62,7 +68,10 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
if deps.gatewayClient != nil {
|
||||
opts = append(opts, orchestrator.WithChainGatewayClient(deps.gatewayClient))
|
||||
}
|
||||
if railGateways := buildRailGateways(deps.gatewayClient, cfg.GatewayInstances); len(railGateways) > 0 {
|
||||
if deps.paymentGatewayClient != nil {
|
||||
opts = append(opts, orchestrator.WithProviderSettlementGatewayClient(deps.paymentGatewayClient))
|
||||
}
|
||||
if railGateways := buildRailGateways(deps.gatewayClient, deps.paymentGatewayClient, cfg.GatewayInstances); len(railGateways) > 0 {
|
||||
opts = append(opts, orchestrator.WithRailGateways(railGateways))
|
||||
}
|
||||
if deps.mntxClient != nil {
|
||||
@@ -77,7 +86,7 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
|
||||
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
|
||||
}
|
||||
if registry := buildGatewayRegistry(i.logger, deps.mntxClient, cfg.GatewayInstances, i.discoveryReg); registry != nil {
|
||||
if registry := buildGatewayRegistry(i.logger, cfg.GatewayInstances, i.discoveryReg); registry != nil {
|
||||
opts = append(opts, orchestrator.WithGatewayRegistry(registry))
|
||||
}
|
||||
return opts
|
||||
|
||||
@@ -18,15 +18,16 @@ type Imp struct {
|
||||
file string
|
||||
debug bool
|
||||
|
||||
config *config
|
||||
app *grpcapp.App[storage.Repository]
|
||||
discoveryWatcher *discovery.RegistryWatcher
|
||||
discoveryReg *discovery.Registry
|
||||
discoveryAnnouncer *discovery.Announcer
|
||||
service *orchestrator.Service
|
||||
feesConn *grpc.ClientConn
|
||||
ledgerClient ledgerclient.Client
|
||||
gatewayClient chainclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
config *config
|
||||
app *grpcapp.App[storage.Repository]
|
||||
discoveryWatcher *discovery.RegistryWatcher
|
||||
discoveryReg *discovery.Registry
|
||||
discoveryAnnouncer *discovery.Announcer
|
||||
service *orchestrator.Service
|
||||
feesConn *grpc.ClientConn
|
||||
ledgerClient ledgerclient.Client
|
||||
gatewayClient chainclient.Client
|
||||
paymentGatewayClient chainclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"strings"
|
||||
|
||||
paymodel "github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
||||
np "github.com/tech/sendico/pkg/messaging/notifications/processor"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"go.uber.org/zap"
|
||||
@@ -47,7 +47,8 @@ func (s *Service) onGatewayExecution(ctx context.Context, exec *model.PaymentGat
|
||||
if s.storage == nil || s.storage.Payments() == nil {
|
||||
return errStorageUnavailable
|
||||
}
|
||||
payment, err := s.storage.Payments().GetByPaymentRef(ctx, paymentRef)
|
||||
store := s.storage.Payments()
|
||||
payment, err := store.GetByPaymentRef(ctx, paymentRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -66,8 +67,12 @@ func (s *Service) onGatewayExecution(ctx context.Context, exec *model.PaymentGat
|
||||
}
|
||||
payment.Metadata["gateway_confirmation_status"] = string(exec.Status)
|
||||
|
||||
updatedPlan := updateExecutionStepsFromGatewayExecution(payment, exec)
|
||||
switch exec.Status {
|
||||
case model.ConfirmationStatusConfirmed, model.ConfirmationStatusClarified:
|
||||
if payment.PaymentPlan != nil && updatedPlan && payment.ExecutionPlan != nil && !executionPlanComplete(payment.ExecutionPlan) {
|
||||
return s.resumePaymentPlan(ctx, store, payment)
|
||||
}
|
||||
payment.State = paymodel.PaymentStateSettled
|
||||
payment.FailureCode = paymodel.PaymentFailureCodeUnspecified
|
||||
payment.FailureReason = ""
|
||||
@@ -82,13 +87,69 @@ func (s *Service) onGatewayExecution(ctx context.Context, exec *model.PaymentGat
|
||||
default:
|
||||
s.logger.Warn("Unhandled gateway confirmation status", zap.String("status", string(exec.Status)), zap.String("payment_ref", paymentRef))
|
||||
}
|
||||
if err := s.storage.Payments().Update(ctx, payment); err != nil {
|
||||
if err := store.Update(ctx, payment); err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("Payment gateway execution applied", zap.String("payment_ref", paymentRef), zap.String("status", string(exec.Status)), zap.String("service", string(mservice.PaymentGateway)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateExecutionStepsFromGatewayExecution(payment *paymodel.Payment, exec *model.PaymentGatewayExecution) bool {
|
||||
if payment == nil || exec == nil || payment.PaymentPlan == nil {
|
||||
return false
|
||||
}
|
||||
requestID := strings.TrimSpace(exec.RequestID)
|
||||
if requestID == "" {
|
||||
return false
|
||||
}
|
||||
execPlan := ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
|
||||
if execPlan == nil {
|
||||
return false
|
||||
}
|
||||
status := executionStepStatusFromGatewayStatus(exec.Status)
|
||||
if status == "" {
|
||||
return false
|
||||
}
|
||||
updated := false
|
||||
for idx, planStep := range payment.PaymentPlan.Steps {
|
||||
if planStep == nil {
|
||||
continue
|
||||
}
|
||||
if idx >= len(execPlan.Steps) {
|
||||
continue
|
||||
}
|
||||
execStep := execPlan.Steps[idx]
|
||||
if execStep == nil {
|
||||
execStep = &paymodel.ExecutionStep{Code: planStepID(planStep, idx), Description: describePlanStep(planStep)}
|
||||
execPlan.Steps[idx] = execStep
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(execStep.TransferRef), requestID) {
|
||||
setExecutionStepStatus(execStep, status)
|
||||
updated = true
|
||||
continue
|
||||
}
|
||||
if execStep.TransferRef == "" && planStep.Rail == paymodel.RailProviderSettlement {
|
||||
if planStep.Action == paymodel.RailOperationObserveConfirm || planStep.Action == paymodel.RailOperationSend {
|
||||
execStep.TransferRef = requestID
|
||||
setExecutionStepStatus(execStep, status)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func executionStepStatusFromGatewayStatus(status model.ConfirmationStatus) string {
|
||||
switch status {
|
||||
case model.ConfirmationStatusConfirmed, model.ConfirmationStatusClarified:
|
||||
return executionStepStatusConfirmed
|
||||
case model.ConfirmationStatusRejected, model.ConfirmationStatusTimeout:
|
||||
return executionStepStatusFailed
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Shutdown() {
|
||||
if s == nil {
|
||||
return
|
||||
|
||||
@@ -5,23 +5,19 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type gatewayRegistry struct {
|
||||
logger mlogger.Logger
|
||||
mntx mntxclient.Client
|
||||
static []*model.GatewayInstanceDescriptor
|
||||
}
|
||||
|
||||
// NewGatewayRegistry aggregates static and remote gateway descriptors.
|
||||
func NewGatewayRegistry(logger mlogger.Logger, mntxClient mntxclient.Client, static []*model.GatewayInstanceDescriptor) GatewayRegistry {
|
||||
if mntxClient == nil && len(static) == 0 {
|
||||
// NewGatewayRegistry aggregates static gateway descriptors.
|
||||
func NewGatewayRegistry(logger mlogger.Logger, static []*model.GatewayInstanceDescriptor) GatewayRegistry {
|
||||
if len(static) == 0 {
|
||||
return nil
|
||||
}
|
||||
if logger != nil {
|
||||
@@ -29,7 +25,6 @@ func NewGatewayRegistry(logger mlogger.Logger, mntxClient mntxclient.Client, sta
|
||||
}
|
||||
return &gatewayRegistry{
|
||||
logger: logger,
|
||||
mntx: mntxClient,
|
||||
static: cloneGatewayDescriptors(static),
|
||||
}
|
||||
}
|
||||
@@ -47,27 +42,6 @@ func (r *gatewayRegistry) List(ctx context.Context) ([]*model.GatewayInstanceDes
|
||||
items[id] = cloneGatewayDescriptor(gw)
|
||||
}
|
||||
|
||||
if r.mntx != nil {
|
||||
resp, err := r.mntx.ListGatewayInstances(ctx, &mntxv1.ListGatewayInstancesRequest{})
|
||||
if err != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Warn("Failed to list Monetix gateway instances", zap.Error(err))
|
||||
}
|
||||
} else {
|
||||
for _, gw := range resp.GetItems() {
|
||||
modelGw := modelGatewayFromProto(gw)
|
||||
if modelGw == nil {
|
||||
continue
|
||||
}
|
||||
id := strings.TrimSpace(modelGw.ID)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
items[id] = modelGw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]*model.GatewayInstanceDescriptor, 0, len(items))
|
||||
for _, gw := range items {
|
||||
result = append(result, gw)
|
||||
|
||||
@@ -48,14 +48,15 @@ func (g gatewayDependency) available() bool {
|
||||
}
|
||||
|
||||
type railGatewayDependency struct {
|
||||
byID map[string]rail.RailGateway
|
||||
byRail map[model.Rail][]rail.RailGateway
|
||||
registry GatewayRegistry
|
||||
chainClient chainclient.Client
|
||||
byID map[string]rail.RailGateway
|
||||
byRail map[model.Rail][]rail.RailGateway
|
||||
registry GatewayRegistry
|
||||
chainClient chainclient.Client
|
||||
providerClient chainclient.Client
|
||||
}
|
||||
|
||||
func (g railGatewayDependency) available() bool {
|
||||
return len(g.byID) > 0 || len(g.byRail) > 0 || (g.registry != nil && g.chainClient != nil)
|
||||
return len(g.byID) > 0 || len(g.byRail) > 0 || (g.registry != nil && (g.chainClient != nil || g.providerClient != nil))
|
||||
}
|
||||
|
||||
func (g railGatewayDependency) resolve(ctx context.Context, step *model.PaymentStep) (rail.RailGateway, error) {
|
||||
@@ -80,7 +81,7 @@ func (g railGatewayDependency) resolve(ctx context.Context, step *model.PaymentS
|
||||
}
|
||||
|
||||
func (g railGatewayDependency) resolveDynamic(ctx context.Context, step *model.PaymentStep) (rail.RailGateway, error) {
|
||||
if g.registry == nil || g.chainClient == nil {
|
||||
if g.registry == nil || (g.chainClient == nil && g.providerClient == nil) {
|
||||
return nil, merrors.InvalidArgument("rail gateway: missing gateway for rail")
|
||||
}
|
||||
items, err := g.registry.List(ctx)
|
||||
@@ -108,7 +109,18 @@ func (g railGatewayDependency) resolveDynamic(ctx context.Context, step *model.P
|
||||
RequiresObserveConfirm: entry.Capabilities.RequiresObserveConfirm,
|
||||
},
|
||||
}
|
||||
return chainclient.NewRailGateway(g.chainClient, cfg), nil
|
||||
switch entry.Rail {
|
||||
case model.RailProviderSettlement:
|
||||
if g.providerClient == nil {
|
||||
return nil, merrors.InvalidArgument("rail gateway: missing provider settlement client")
|
||||
}
|
||||
return NewProviderSettlementGateway(g.providerClient, cfg), nil
|
||||
default:
|
||||
if g.chainClient == nil {
|
||||
return nil, merrors.InvalidArgument("rail gateway: missing gateway client")
|
||||
}
|
||||
return chainclient.NewRailGateway(g.chainClient, cfg), nil
|
||||
}
|
||||
}
|
||||
return nil, merrors.InvalidArgument("rail gateway: missing gateway for rail")
|
||||
}
|
||||
@@ -137,6 +149,14 @@ func (g gatewayRegistryDependency) available() bool {
|
||||
return g.registry != nil
|
||||
}
|
||||
|
||||
type providerGatewayDependency struct {
|
||||
client chainclient.Client
|
||||
}
|
||||
|
||||
func (p providerGatewayDependency) available() bool {
|
||||
return p.client != nil
|
||||
}
|
||||
|
||||
// CardGatewayRoute maps a gateway to its funding and fee destinations.
|
||||
type CardGatewayRoute struct {
|
||||
FundingAddress string
|
||||
@@ -179,13 +199,20 @@ func WithChainGatewayClient(client chainclient.Client) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithProviderSettlementGatewayClient wires the provider settlement gateway client.
|
||||
func WithProviderSettlementGatewayClient(client chainclient.Client) Option {
|
||||
return func(s *Service) {
|
||||
s.deps.providerGateway = providerGatewayDependency{client: client}
|
||||
}
|
||||
}
|
||||
|
||||
// WithRailGateways wires rail gateway adapters by instance ID.
|
||||
func WithRailGateways(gateways map[string]rail.RailGateway) Option {
|
||||
return func(s *Service) {
|
||||
if len(gateways) == 0 {
|
||||
return
|
||||
}
|
||||
s.deps.railGateways = buildRailGatewayDependency(gateways, s.deps.gatewayRegistry, s.deps.gateway.client)
|
||||
s.deps.railGateways = buildRailGatewayDependency(gateways, s.deps.gatewayRegistry, s.deps.gateway.client, s.deps.providerGateway.client)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +277,7 @@ func WithGatewayRegistry(registry GatewayRegistry) Option {
|
||||
s.deps.gatewayRegistry = registry
|
||||
s.deps.railGateways.registry = registry
|
||||
s.deps.railGateways.chainClient = s.deps.gateway.client
|
||||
s.deps.railGateways.providerClient = s.deps.providerGateway.client
|
||||
if s.deps.planBuilder == nil {
|
||||
s.deps.planBuilder = &defaultPlanBuilder{}
|
||||
}
|
||||
@@ -266,12 +294,13 @@ func WithClock(clock clockpkg.Clock) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func buildRailGatewayDependency(gateways map[string]rail.RailGateway, registry GatewayRegistry, chainClient chainclient.Client) railGatewayDependency {
|
||||
func buildRailGatewayDependency(gateways map[string]rail.RailGateway, registry GatewayRegistry, chainClient chainclient.Client, providerClient chainclient.Client) railGatewayDependency {
|
||||
result := railGatewayDependency{
|
||||
byID: map[string]rail.RailGateway{},
|
||||
byRail: map[model.Rail][]rail.RailGateway{},
|
||||
registry: registry,
|
||||
chainClient: chainClient,
|
||||
byID: map[string]rail.RailGateway{},
|
||||
byRail: map[model.Rail][]rail.RailGateway{},
|
||||
registry: registry,
|
||||
chainClient: chainClient,
|
||||
providerClient: providerClient,
|
||||
}
|
||||
if len(gateways) == 0 {
|
||||
return result
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestExecutePaymentPlan_SourceBeforeDestination(t *testing.T) {
|
||||
deps: serviceDependencies{
|
||||
railGateways: buildRailGatewayDependency(map[string]rail.RailGateway{
|
||||
"crypto-default": railGateway,
|
||||
}, nil, nil),
|
||||
}, nil, nil, nil),
|
||||
ledger: ledgerDependency{
|
||||
client: ledgerFake,
|
||||
internal: ledgerFake,
|
||||
|
||||
@@ -100,6 +100,33 @@ func (p *paymentExecutor) executeSendStep(ctx context.Context, payment *model.Pa
|
||||
ensureExecutionRefs(payment).CardPayoutRef = ref
|
||||
setExecutionStepStatus(execStep, executionStepStatusSubmitted)
|
||||
return true, nil
|
||||
case model.RailProviderSettlement:
|
||||
amount, err := requireMoney(cloneMoney(step.Amount), "provider settlement amount")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !p.deps.railGateways.available() {
|
||||
return false, merrors.Internal("rail gateway unavailable")
|
||||
}
|
||||
req, err := p.buildProviderSettlementTransferRequest(payment, step, amount, quote, idx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
gw, err := p.deps.railGateways.resolve(ctx, step)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result, err := gw.Send(ctx, req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
execStep.TransferRef = strings.TrimSpace(result.ReferenceID)
|
||||
if execStep.TransferRef == "" {
|
||||
execStep.TransferRef = strings.TrimSpace(req.IdempotencyKey)
|
||||
}
|
||||
linkProviderSettlementObservation(payment, execStep.TransferRef)
|
||||
setExecutionStepStatus(execStep, executionStepStatusSubmitted)
|
||||
return true, nil
|
||||
case model.RailFiatOnRamp:
|
||||
return false, merrors.InvalidArgument("payment plan: fiat on-ramp execution not implemented")
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/payments/rail"
|
||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
providerSettlementMetaPaymentIntentID = "payment_intent_id"
|
||||
providerSettlementMetaQuoteRef = "quote_ref"
|
||||
providerSettlementMetaTargetChatID = "target_chat_id"
|
||||
providerSettlementMetaOutgoingLeg = "outgoing_leg"
|
||||
)
|
||||
|
||||
func (p *paymentExecutor) buildProviderSettlementTransferRequest(payment *model.Payment, step *model.PaymentStep, amount *paymenttypes.Money, quote *orchestratorv1.PaymentQuote, idx int) (rail.TransferRequest, error) {
|
||||
if payment == nil || step == nil {
|
||||
return rail.TransferRequest{}, merrors.InvalidArgument("provider settlement: payment and step are required")
|
||||
}
|
||||
if amount == nil {
|
||||
return rail.TransferRequest{}, merrors.InvalidArgument("provider settlement: amount is required")
|
||||
}
|
||||
requestID := planStepIdempotencyKey(payment, idx, step)
|
||||
if requestID == "" {
|
||||
return rail.TransferRequest{}, merrors.InvalidArgument("provider settlement: idempotency key is required")
|
||||
}
|
||||
paymentRef := strings.TrimSpace(payment.PaymentRef)
|
||||
if paymentRef == "" {
|
||||
return rail.TransferRequest{}, merrors.InvalidArgument("provider settlement: payment_ref is required")
|
||||
}
|
||||
metadata := cloneMetadata(payment.Metadata)
|
||||
if metadata == nil {
|
||||
metadata = map[string]string{}
|
||||
}
|
||||
metadata[providerSettlementMetaPaymentIntentID] = paymentRef
|
||||
if quoteRef := paymentGatewayQuoteRef(payment, quote); quoteRef != "" {
|
||||
metadata[providerSettlementMetaQuoteRef] = quoteRef
|
||||
}
|
||||
if chatID := paymentGatewayTargetChatID(payment); chatID != "" {
|
||||
metadata[providerSettlementMetaTargetChatID] = chatID
|
||||
}
|
||||
if strings.TrimSpace(metadata[providerSettlementMetaOutgoingLeg]) == "" {
|
||||
metadata[providerSettlementMetaOutgoingLeg] = strings.ToLower(strings.TrimSpace(string(step.Rail)))
|
||||
}
|
||||
return rail.TransferRequest{
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
Currency: strings.TrimSpace(amount.GetCurrency()),
|
||||
Amount: strings.TrimSpace(amount.GetAmount()),
|
||||
IdempotencyKey: requestID,
|
||||
Metadata: metadata,
|
||||
ClientReference: paymentRef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func paymentGatewayQuoteRef(payment *model.Payment, quote *orchestratorv1.PaymentQuote) string {
|
||||
if quote != nil {
|
||||
if ref := strings.TrimSpace(quote.GetQuoteRef()); ref != "" {
|
||||
return ref
|
||||
}
|
||||
}
|
||||
if payment != nil && payment.LastQuote != nil {
|
||||
return strings.TrimSpace(payment.LastQuote.QuoteRef)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func paymentGatewayTargetChatID(payment *model.Payment) string {
|
||||
if payment == nil {
|
||||
return ""
|
||||
}
|
||||
if payment.Intent.Attributes != nil {
|
||||
if chatID := strings.TrimSpace(payment.Intent.Attributes["target_chat_id"]); chatID != "" {
|
||||
return chatID
|
||||
}
|
||||
}
|
||||
if payment.Metadata != nil {
|
||||
return strings.TrimSpace(payment.Metadata["target_chat_id"])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func linkProviderSettlementObservation(payment *model.Payment, requestID string) {
|
||||
if payment == nil || payment.PaymentPlan == nil || payment.ExecutionPlan == nil {
|
||||
return
|
||||
}
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" {
|
||||
return
|
||||
}
|
||||
for idx, planStep := range payment.PaymentPlan.Steps {
|
||||
if planStep == nil {
|
||||
continue
|
||||
}
|
||||
if planStep.Rail != model.RailProviderSettlement || planStep.Action != model.RailOperationObserveConfirm {
|
||||
continue
|
||||
}
|
||||
if idx >= len(payment.ExecutionPlan.Steps) {
|
||||
continue
|
||||
}
|
||||
execStep := payment.ExecutionPlan.Steps[idx]
|
||||
if execStep == nil {
|
||||
execStep = &model.ExecutionStep{Code: planStepID(planStep, idx), Description: describePlanStep(planStep)}
|
||||
payment.ExecutionPlan.Steps[idx] = execStep
|
||||
}
|
||||
if execStep.TransferRef == "" {
|
||||
execStep.TransferRef = requestID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/payments/rail"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
)
|
||||
|
||||
type providerSettlementGateway struct {
|
||||
client chainclient.Client
|
||||
rail string
|
||||
network string
|
||||
capabilities rail.RailCapabilities
|
||||
}
|
||||
|
||||
func NewProviderSettlementGateway(client chainclient.Client, cfg chainclient.RailGatewayConfig) rail.RailGateway {
|
||||
railName := strings.ToUpper(strings.TrimSpace(cfg.Rail))
|
||||
if railName == "" {
|
||||
railName = "PROVIDER_SETTLEMENT"
|
||||
}
|
||||
return &providerSettlementGateway{
|
||||
client: client,
|
||||
rail: railName,
|
||||
network: strings.ToUpper(strings.TrimSpace(cfg.Network)),
|
||||
capabilities: cfg.Capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *providerSettlementGateway) Rail() string {
|
||||
return g.rail
|
||||
}
|
||||
|
||||
func (g *providerSettlementGateway) Network() string {
|
||||
return g.network
|
||||
}
|
||||
|
||||
func (g *providerSettlementGateway) Capabilities() rail.RailCapabilities {
|
||||
return g.capabilities
|
||||
}
|
||||
|
||||
func (g *providerSettlementGateway) Send(ctx context.Context, req rail.TransferRequest) (rail.RailResult, error) {
|
||||
if g.client == nil {
|
||||
return rail.RailResult{}, merrors.Internal("provider settlement gateway: client is required")
|
||||
}
|
||||
idempotencyKey := strings.TrimSpace(req.IdempotencyKey)
|
||||
if idempotencyKey == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("provider settlement gateway: idempotency_key is required")
|
||||
}
|
||||
currency := strings.TrimSpace(req.Currency)
|
||||
amount := strings.TrimSpace(req.Amount)
|
||||
if currency == "" || amount == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("provider settlement gateway: amount is required")
|
||||
}
|
||||
metadata := cloneMetadata(req.Metadata)
|
||||
if metadata == nil {
|
||||
metadata = map[string]string{}
|
||||
}
|
||||
if strings.TrimSpace(metadata[providerSettlementMetaPaymentIntentID]) == "" {
|
||||
if ref := strings.TrimSpace(req.ClientReference); ref != "" {
|
||||
metadata[providerSettlementMetaPaymentIntentID] = ref
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(metadata[providerSettlementMetaPaymentIntentID]) == "" {
|
||||
return rail.RailResult{}, merrors.InvalidArgument("provider settlement gateway: payment_intent_id is required")
|
||||
}
|
||||
if strings.TrimSpace(metadata[providerSettlementMetaOutgoingLeg]) == "" && g.rail != "" {
|
||||
metadata[providerSettlementMetaOutgoingLeg] = strings.ToLower(strings.TrimSpace(g.rail))
|
||||
}
|
||||
submitReq := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: strings.TrimSpace(req.OrganizationRef),
|
||||
SourceWalletRef: strings.TrimSpace(req.FromAccountID),
|
||||
Amount: &moneyv1.Money{
|
||||
Currency: currency,
|
||||
Amount: amount,
|
||||
},
|
||||
Metadata: metadata,
|
||||
ClientReference: strings.TrimSpace(req.ClientReference),
|
||||
}
|
||||
if dest := buildProviderSettlementDestination(req); dest != nil {
|
||||
submitReq.Destination = dest
|
||||
}
|
||||
resp, err := g.client.SubmitTransfer(ctx, submitReq)
|
||||
if err != nil {
|
||||
return rail.RailResult{}, err
|
||||
}
|
||||
if resp == nil || resp.GetTransfer() == nil {
|
||||
return rail.RailResult{}, merrors.Internal("provider settlement gateway: missing transfer response")
|
||||
}
|
||||
transfer := resp.GetTransfer()
|
||||
return rail.RailResult{
|
||||
ReferenceID: strings.TrimSpace(transfer.GetTransferRef()),
|
||||
Status: providerSettlementStatusFromTransfer(transfer.GetStatus()),
|
||||
FinalAmount: railMoneyFromProto(transfer.GetNetAmount()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *providerSettlementGateway) Observe(ctx context.Context, referenceID string) (rail.ObserveResult, error) {
|
||||
if g.client == nil {
|
||||
return rail.ObserveResult{}, merrors.Internal("provider settlement gateway: client is required")
|
||||
}
|
||||
ref := strings.TrimSpace(referenceID)
|
||||
if ref == "" {
|
||||
return rail.ObserveResult{}, merrors.InvalidArgument("provider settlement gateway: reference_id is required")
|
||||
}
|
||||
resp, err := g.client.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: ref})
|
||||
if err != nil {
|
||||
return rail.ObserveResult{}, err
|
||||
}
|
||||
if resp == nil || resp.GetTransfer() == nil {
|
||||
return rail.ObserveResult{}, merrors.Internal("provider settlement gateway: missing transfer response")
|
||||
}
|
||||
transfer := resp.GetTransfer()
|
||||
return rail.ObserveResult{
|
||||
ReferenceID: ref,
|
||||
Status: providerSettlementStatusFromTransfer(transfer.GetStatus()),
|
||||
FinalAmount: railMoneyFromProto(transfer.GetNetAmount()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildProviderSettlementDestination(req rail.TransferRequest) *chainv1.TransferDestination {
|
||||
destRef := strings.TrimSpace(req.ToAccountID)
|
||||
memo := strings.TrimSpace(req.DestinationMemo)
|
||||
if destRef == "" && memo == "" {
|
||||
return nil
|
||||
}
|
||||
return &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: destRef},
|
||||
Memo: memo,
|
||||
}
|
||||
}
|
||||
|
||||
func providerSettlementStatusFromTransfer(status chainv1.TransferStatus) string {
|
||||
switch status {
|
||||
case chainv1.TransferStatus_TRANSFER_CONFIRMED:
|
||||
return rail.TransferStatusSuccess
|
||||
case chainv1.TransferStatus_TRANSFER_FAILED:
|
||||
return rail.TransferStatusFailed
|
||||
case chainv1.TransferStatus_TRANSFER_CANCELLED:
|
||||
return rail.TransferStatusRejected
|
||||
default:
|
||||
return rail.TransferStatusPending
|
||||
}
|
||||
}
|
||||
|
||||
func railMoneyFromProto(src *moneyv1.Money) *rail.Money {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
currency := strings.TrimSpace(src.GetCurrency())
|
||||
amount := strings.TrimSpace(src.GetAmount())
|
||||
if currency == "" || amount == "" {
|
||||
return nil
|
||||
}
|
||||
return &rail.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ type serviceDependencies struct {
|
||||
ledger ledgerDependency
|
||||
gateway gatewayDependency
|
||||
railGateways railGatewayDependency
|
||||
providerGateway providerGatewayDependency
|
||||
oracle oracleDependency
|
||||
mntx mntxDependency
|
||||
gatewayRegistry GatewayRegistry
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestExecutePayment_ChainFailure(t *testing.T) {
|
||||
return rail.RailResult{}, errors.New("chain failure")
|
||||
},
|
||||
},
|
||||
}, nil, nil),
|
||||
}, nil, nil, nil),
|
||||
gatewayRegistry: &stubGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
|
||||
@@ -231,19 +231,3 @@ message TransferStatusChangedEvent {
|
||||
Transfer transfer = 1;
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
service ChainGatewayService {
|
||||
rpc CreateManagedWallet(CreateManagedWalletRequest) returns (CreateManagedWalletResponse);
|
||||
rpc GetManagedWallet(GetManagedWalletRequest) returns (GetManagedWalletResponse);
|
||||
rpc ListManagedWallets(ListManagedWalletsRequest) returns (ListManagedWalletsResponse);
|
||||
|
||||
rpc GetWalletBalance(GetWalletBalanceRequest) returns (GetWalletBalanceResponse);
|
||||
|
||||
rpc SubmitTransfer(SubmitTransferRequest) returns (SubmitTransferResponse);
|
||||
rpc GetTransfer(GetTransferRequest) returns (GetTransferResponse);
|
||||
rpc ListTransfers(ListTransfersRequest) returns (ListTransfersResponse);
|
||||
|
||||
rpc EstimateTransferFee(EstimateTransferFeeRequest) returns (EstimateTransferFeeResponse);
|
||||
rpc ComputeGasTopUp(ComputeGasTopUpRequest) returns (ComputeGasTopUpResponse);
|
||||
rpc EnsureGasTopUp(EnsureGasTopUpRequest) returns (EnsureGasTopUpResponse);
|
||||
}
|
||||
|
||||
@@ -164,11 +164,3 @@ message CardTokenizeResponse {
|
||||
string error_code = 8;
|
||||
string error_message = 9;
|
||||
}
|
||||
|
||||
service MntxGatewayService {
|
||||
rpc CreateCardPayout(CardPayoutRequest) returns (CardPayoutResponse);
|
||||
rpc GetCardPayoutStatus(GetCardPayoutStatusRequest) returns (GetCardPayoutStatusResponse);
|
||||
rpc CreateCardTokenPayout(CardTokenPayoutRequest) returns (CardTokenPayoutResponse);
|
||||
rpc CreateCardToken(CardTokenizeRequest) returns (CardTokenizeResponse);
|
||||
rpc ListGatewayInstances(ListGatewayInstancesRequest) returns (ListGatewayInstancesResponse);
|
||||
}
|
||||
|
||||
45
api/proto/gateway/unified/v1/gateway.proto
Normal file
45
api/proto/gateway/unified/v1/gateway.proto
Normal file
@@ -0,0 +1,45 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package gateway.unified.v1;
|
||||
|
||||
option go_package = "github.com/tech/sendico/pkg/proto/gateway/unified/v1;unifiedv1";
|
||||
|
||||
import "gateway/chain/v1/chain.proto";
|
||||
import "gateway/mntx/v1/mntx.proto";
|
||||
import "ledger/v1/ledger.proto";
|
||||
|
||||
// UnifiedGatewayService exposes gateway and ledger operations via a single interface.
|
||||
service UnifiedGatewayService {
|
||||
// Chain gateway operations.
|
||||
rpc CreateManagedWallet(chain.gateway.v1.CreateManagedWalletRequest) returns (chain.gateway.v1.CreateManagedWalletResponse);
|
||||
rpc GetManagedWallet(chain.gateway.v1.GetManagedWalletRequest) returns (chain.gateway.v1.GetManagedWalletResponse);
|
||||
rpc ListManagedWallets(chain.gateway.v1.ListManagedWalletsRequest) returns (chain.gateway.v1.ListManagedWalletsResponse);
|
||||
rpc GetWalletBalance(chain.gateway.v1.GetWalletBalanceRequest) returns (chain.gateway.v1.GetWalletBalanceResponse);
|
||||
|
||||
rpc SubmitTransfer(chain.gateway.v1.SubmitTransferRequest) returns (chain.gateway.v1.SubmitTransferResponse);
|
||||
rpc GetTransfer(chain.gateway.v1.GetTransferRequest) returns (chain.gateway.v1.GetTransferResponse);
|
||||
rpc ListTransfers(chain.gateway.v1.ListTransfersRequest) returns (chain.gateway.v1.ListTransfersResponse);
|
||||
|
||||
rpc EstimateTransferFee(chain.gateway.v1.EstimateTransferFeeRequest) returns (chain.gateway.v1.EstimateTransferFeeResponse);
|
||||
rpc ComputeGasTopUp(chain.gateway.v1.ComputeGasTopUpRequest) returns (chain.gateway.v1.ComputeGasTopUpResponse);
|
||||
rpc EnsureGasTopUp(chain.gateway.v1.EnsureGasTopUpRequest) returns (chain.gateway.v1.EnsureGasTopUpResponse);
|
||||
|
||||
// Card payout gateway operations.
|
||||
rpc CreateCardPayout(mntx.gateway.v1.CardPayoutRequest) returns (mntx.gateway.v1.CardPayoutResponse);
|
||||
rpc GetCardPayoutStatus(mntx.gateway.v1.GetCardPayoutStatusRequest) returns (mntx.gateway.v1.GetCardPayoutStatusResponse);
|
||||
rpc CreateCardTokenPayout(mntx.gateway.v1.CardTokenPayoutRequest) returns (mntx.gateway.v1.CardTokenPayoutResponse);
|
||||
rpc CreateCardToken(mntx.gateway.v1.CardTokenizeRequest) returns (mntx.gateway.v1.CardTokenizeResponse);
|
||||
rpc ListGatewayInstances(mntx.gateway.v1.ListGatewayInstancesRequest) returns (mntx.gateway.v1.ListGatewayInstancesResponse);
|
||||
|
||||
// Ledger operations.
|
||||
rpc CreateAccount(ledger.v1.CreateAccountRequest) returns (ledger.v1.CreateAccountResponse);
|
||||
rpc ListAccounts(ledger.v1.ListAccountsRequest) returns (ledger.v1.ListAccountsResponse);
|
||||
rpc PostCreditWithCharges(ledger.v1.PostCreditRequest) returns (ledger.v1.PostResponse);
|
||||
rpc PostDebitWithCharges(ledger.v1.PostDebitRequest) returns (ledger.v1.PostResponse);
|
||||
rpc TransferInternal(ledger.v1.TransferRequest) returns (ledger.v1.PostResponse);
|
||||
rpc ApplyFXWithCharges(ledger.v1.FXRequest) returns (ledger.v1.PostResponse);
|
||||
|
||||
rpc GetBalance(ledger.v1.GetBalanceRequest) returns (ledger.v1.BalanceResponse);
|
||||
rpc GetJournalEntry(ledger.v1.GetEntryRequest) returns (ledger.v1.JournalEntryResponse);
|
||||
rpc GetStatement(ledger.v1.GetStatementRequest) returns (ledger.v1.StatementResponse);
|
||||
}
|
||||
@@ -66,22 +66,6 @@ message PostingLine {
|
||||
|
||||
// ===== Requests/Responses =====
|
||||
|
||||
service LedgerService {
|
||||
rpc CreateAccount (CreateAccountRequest) returns (CreateAccountResponse);
|
||||
|
||||
rpc PostCreditWithCharges (PostCreditRequest) returns (PostResponse);
|
||||
rpc PostDebitWithCharges (PostDebitRequest) returns (PostResponse);
|
||||
rpc TransferInternal (TransferRequest) returns (PostResponse);
|
||||
rpc ApplyFXWithCharges (FXRequest) returns (PostResponse);
|
||||
|
||||
rpc GetBalance (GetBalanceRequest) returns (BalanceResponse);
|
||||
rpc GetJournalEntry (GetEntryRequest) returns (JournalEntryResponse);
|
||||
rpc GetStatement (GetStatementRequest) returns (StatementResponse);
|
||||
|
||||
// Lists ledger accounts for an organization.
|
||||
rpc ListAccounts (ListAccountsRequest) returns (ListAccountsResponse);
|
||||
}
|
||||
|
||||
message CreateAccountRequest {
|
||||
string organization_ref = 1;
|
||||
string account_code = 2;
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type PaymentIntent struct {
|
||||
Kind PaymentKind `json:"kind,omitempty"`
|
||||
Source *Endpoint `json:"source,omitempty"`
|
||||
Destination *Endpoint `json:"destination,omitempty"`
|
||||
Amount *model.Money `json:"amount,omitempty"`
|
||||
FX *FXIntent `json:"fx,omitempty"`
|
||||
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
Kind PaymentKind `json:"kind,omitempty"`
|
||||
Source *Endpoint `json:"source,omitempty"`
|
||||
Destination *Endpoint `json:"destination,omitempty"`
|
||||
Amount *model.Money `json:"amount,omitempty"`
|
||||
FX *FXIntent `json:"fx,omitempty"`
|
||||
SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
|
||||
SettlementCurrency string `json:"settlement_currency,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
Customer *Customer `json:"customer,omitempty"`
|
||||
}
|
||||
|
||||
type AssetResolverStub struct{}
|
||||
@@ -51,5 +54,11 @@ func (p *PaymentIntent) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(p.SettlementCurrency) != "" {
|
||||
if err := ValidateCurrency(p.SettlementCurrency, &AssetResolverStub{}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIn
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settlementCurrency := strings.TrimSpace(intent.SettlementCurrency)
|
||||
if settlementCurrency == "" {
|
||||
settlementCurrency = resolveSettlementCurrency(intent)
|
||||
}
|
||||
|
||||
source, err := mapPaymentEndpoint(intent.Source, "source")
|
||||
if err != nil {
|
||||
@@ -42,18 +46,66 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIn
|
||||
}
|
||||
|
||||
return &orchestratorv1.PaymentIntent{
|
||||
Kind: kind,
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: fx != nil,
|
||||
Fx: fx,
|
||||
SettlementMode: settlementMode,
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
Customer: mapCustomer(intent.Customer),
|
||||
Kind: kind,
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: fx != nil,
|
||||
Fx: fx,
|
||||
SettlementMode: settlementMode,
|
||||
SettlementCurrency: settlementCurrency,
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
Customer: mapCustomer(intent.Customer),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveSettlementCurrency(intent *srequest.PaymentIntent) string {
|
||||
if intent == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fx := intent.FX
|
||||
if fx != nil && fx.Pair != nil {
|
||||
base := strings.TrimSpace(fx.Pair.Base)
|
||||
quote := strings.TrimSpace(fx.Pair.Quote)
|
||||
switch strings.TrimSpace(string(fx.Side)) {
|
||||
case string(srequest.FXSideBuyBaseSellQuote):
|
||||
if base != "" {
|
||||
return base
|
||||
}
|
||||
case string(srequest.FXSideSellBaseBuyQuote):
|
||||
if quote != "" {
|
||||
return quote
|
||||
}
|
||||
}
|
||||
if intent.Amount != nil {
|
||||
amountCurrency := strings.TrimSpace(intent.Amount.Currency)
|
||||
if amountCurrency != "" {
|
||||
switch {
|
||||
case strings.EqualFold(amountCurrency, base) && quote != "":
|
||||
return quote
|
||||
case strings.EqualFold(amountCurrency, quote) && base != "":
|
||||
return base
|
||||
default:
|
||||
return amountCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
if quote != "" {
|
||||
return quote
|
||||
}
|
||||
if base != "" {
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
if intent.Amount != nil {
|
||||
return strings.TrimSpace(intent.Amount.Currency)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func mapPaymentEndpoint(endpoint *srequest.Endpoint, field string) (*orchestratorv1.PaymentEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil, nil
|
||||
|
||||
@@ -116,6 +116,12 @@ if [ -f "${PROTO_DIR}/gateway/mntx/v1/mntx.proto" ]; then
|
||||
generate_go_with_grpc "${PROTO_DIR}/gateway/mntx/v1/mntx.proto"
|
||||
fi
|
||||
|
||||
if [ -f "${PROTO_DIR}/gateway/unified/v1/gateway.proto" ]; then
|
||||
info "Compiling unified gateway protos"
|
||||
clean_pb_files "./pkg/proto/gateway/unified"
|
||||
generate_go_with_grpc "${PROTO_DIR}/gateway/unified/v1/gateway.proto"
|
||||
fi
|
||||
|
||||
if [ -f "${PROTO_DIR}/payments/orchestrator/v1/orchestrator.proto" ]; then
|
||||
info "Compiling payments orchestrator protos"
|
||||
clean_pb_files "./pkg/proto/payments/orchestrator"
|
||||
|
||||
@@ -20,6 +20,9 @@ class PaymentIntentDTO {
|
||||
@JsonKey(name: 'settlement_mode')
|
||||
final String? settlementMode;
|
||||
|
||||
@JsonKey(name: 'settlement_currency')
|
||||
final String? settlementCurrency;
|
||||
|
||||
final Map<String, String>? attributes;
|
||||
final CustomerDTO? customer;
|
||||
|
||||
@@ -30,6 +33,7 @@ class PaymentIntentDTO {
|
||||
this.amount,
|
||||
this.fx,
|
||||
this.settlementMode,
|
||||
this.settlementCurrency,
|
||||
this.attributes,
|
||||
this.customer,
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ extension PaymentIntentMapper on PaymentIntent {
|
||||
amount: amount?.toDTO(),
|
||||
fx: fx?.toDTO(),
|
||||
settlementMode: settlementModeToValue(settlementMode),
|
||||
settlementCurrency: settlementCurrency,
|
||||
attributes: attributes,
|
||||
customer: customer?.toDTO(),
|
||||
);
|
||||
@@ -28,6 +29,7 @@ extension PaymentIntentDTOMapper on PaymentIntentDTO {
|
||||
amount: amount?.toDomain(),
|
||||
fx: fx?.toDomain(),
|
||||
settlementMode: settlementModeFromValue(settlementMode),
|
||||
settlementCurrency: settlementCurrency,
|
||||
attributes: attributes,
|
||||
customer: customer?.toDomain(),
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ class PaymentIntent {
|
||||
final Money? amount;
|
||||
final FxIntent? fx;
|
||||
final SettlementMode settlementMode;
|
||||
final String? settlementCurrency;
|
||||
final Map<String, String>? attributes;
|
||||
final Customer? customer;
|
||||
|
||||
@@ -23,6 +24,7 @@ class PaymentIntent {
|
||||
this.amount,
|
||||
this.fx,
|
||||
this.settlementMode = SettlementMode.unspecified,
|
||||
this.settlementCurrency,
|
||||
this.attributes,
|
||||
this.customer,
|
||||
});
|
||||
|
||||
@@ -51,25 +51,28 @@ class QuotationProvider extends ChangeNotifier {
|
||||
recipient: recipients.currentObject,
|
||||
method: method,
|
||||
);
|
||||
final amount = Money(
|
||||
amount: payment.amount.toString(),
|
||||
// TODO: adapt to possible other sources
|
||||
currency: currencyCodeToString(wallets.selectedWallet!.currency),
|
||||
);
|
||||
final fxIntent = FxIntent(
|
||||
pair: CurrencyPair(
|
||||
base: currencyCodeToString(wallets.selectedWallet!.currency),
|
||||
quote: 'RUB', // TODO: exentd target currencies
|
||||
),
|
||||
side: FxSide.sellBaseBuyQuote,
|
||||
);
|
||||
getQuotation(PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
amount: Money(
|
||||
amount: payment.amount.toString(),
|
||||
// TODO: adapt to possible other sources
|
||||
currency: currencyCodeToString(wallets.selectedWallet!.currency),
|
||||
),
|
||||
amount: amount,
|
||||
destination: method.data,
|
||||
source: ManagedWalletPaymentMethod(
|
||||
managedWalletRef: wallets.selectedWallet!.id,
|
||||
),
|
||||
fx: FxIntent(
|
||||
pair: CurrencyPair(
|
||||
base: currencyCodeToString(wallets.selectedWallet!.currency),
|
||||
quote: 'RUB', // TODO: exentd target currencies
|
||||
),
|
||||
side: FxSide.sellBaseBuyQuote,
|
||||
),
|
||||
fx: fxIntent,
|
||||
settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource,
|
||||
settlementCurrency: _resolveSettlementCurrency(amount: amount, fx: fxIntent),
|
||||
customer: customer,
|
||||
));
|
||||
}
|
||||
@@ -83,6 +86,30 @@ class QuotationProvider extends ChangeNotifier {
|
||||
Asset? get total => quotation == null ? null : createAsset(quotation!.debitAmount!.currency, quotation!.debitAmount!.amount);
|
||||
Asset? get recipientGets => quotation == null ? null : createAsset(quotation!.expectedSettlementAmount!.currency, quotation!.expectedSettlementAmount!.amount);
|
||||
|
||||
String _resolveSettlementCurrency({
|
||||
required Money amount,
|
||||
required FxIntent? fx,
|
||||
}) {
|
||||
final pair = fx?.pair;
|
||||
if (pair != null) {
|
||||
switch (fx?.side ?? FxSide.unspecified) {
|
||||
case FxSide.buyBaseSellQuote:
|
||||
if (pair.base.isNotEmpty) return pair.base;
|
||||
break;
|
||||
case FxSide.sellBaseBuyQuote:
|
||||
if (pair.quote.isNotEmpty) return pair.quote;
|
||||
break;
|
||||
case FxSide.unspecified:
|
||||
break;
|
||||
}
|
||||
if (amount.currency == pair.base && pair.quote.isNotEmpty) return pair.quote;
|
||||
if (amount.currency == pair.quote && pair.base.isNotEmpty) return pair.base;
|
||||
if (pair.quote.isNotEmpty) return pair.quote;
|
||||
if (pair.base.isNotEmpty) return pair.base;
|
||||
}
|
||||
return amount.currency;
|
||||
}
|
||||
|
||||
Customer _buildCustomer({
|
||||
required Recipient? recipient,
|
||||
required PaymentMethod method,
|
||||
|
||||
Reference in New Issue
Block a user