unified gateway interfaces
This commit is contained in:
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user