payment rails
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
package paymentapiimp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
@@ -15,6 +16,16 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIn
|
||||
return nil, merrors.InvalidArgument("intent is required")
|
||||
}
|
||||
|
||||
kind, err := mapPaymentKind(intent.Kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settlementMode, err := mapSettlementMode(intent.SettlementMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source, err := mapPaymentEndpoint(intent.Source, "source")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -30,48 +41,68 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIn
|
||||
}
|
||||
|
||||
return &orchestratorv1.PaymentIntent{
|
||||
Kind: orchestratorv1.PaymentKind(intent.Kind),
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: intent.RequiresFX,
|
||||
Fx: fx,
|
||||
FeePolicy: mapPolicyOverrides(intent.FeePolicy),
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
Kind: kind,
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: intent.RequiresFX,
|
||||
Fx: fx,
|
||||
SettlementMode: settlementMode,
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapPaymentEndpoint(endpoint *srequest.PaymentEndpoint, field string) (*orchestratorv1.PaymentEndpoint, error) {
|
||||
func mapPaymentEndpoint(endpoint *srequest.Endpoint, field string) (*orchestratorv1.PaymentEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
count int
|
||||
result orchestratorv1.PaymentEndpoint
|
||||
)
|
||||
|
||||
if endpoint.Ledger != nil {
|
||||
count++
|
||||
var result orchestratorv1.PaymentEndpoint
|
||||
switch endpoint.Type {
|
||||
case srequest.EndpointTypeLedger:
|
||||
payload, err := endpoint.DecodeLedger()
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{
|
||||
Ledger: mapLedgerEndpoint(endpoint.Ledger),
|
||||
Ledger: mapLedgerEndpoint(&payload),
|
||||
}
|
||||
case srequest.EndpointTypeManagedWallet:
|
||||
payload, err := endpoint.DecodeManagedWallet()
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
mw, err := mapManagedWalletEndpoint(&payload)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
}
|
||||
if endpoint.ManagedWallet != nil {
|
||||
count++
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{
|
||||
ManagedWallet: mapManagedWalletEndpoint(endpoint.ManagedWallet),
|
||||
ManagedWallet: mw,
|
||||
}
|
||||
case srequest.EndpointTypeExternalChain:
|
||||
payload, err := endpoint.DecodeExternalChain()
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
ext, err := mapExternalChainEndpoint(&payload)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
}
|
||||
if endpoint.ExternalChain != nil {
|
||||
count++
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{
|
||||
ExternalChain: mapExternalChainEndpoint(endpoint.ExternalChain),
|
||||
ExternalChain: ext,
|
||||
}
|
||||
}
|
||||
|
||||
if count > 1 {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint must set only one of ledger, managed_wallet, external_chain")
|
||||
case srequest.EndpointTypeCard:
|
||||
payload, err := endpoint.DecodeCard()
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
result.Endpoint = &orchestratorv1.PaymentEndpoint_Card{
|
||||
Card: mapCardEndpoint(&payload),
|
||||
}
|
||||
case "":
|
||||
return nil, merrors.InvalidArgument(field + " endpoint type is required")
|
||||
default:
|
||||
return nil, merrors.InvalidArgument(field + " endpoint has unsupported type: " + string(endpoint.Type))
|
||||
}
|
||||
|
||||
result.Metadata = copyStringMap(endpoint.Metadata)
|
||||
@@ -88,36 +119,48 @@ func mapLedgerEndpoint(endpoint *srequest.LedgerEndpoint) *orchestratorv1.Ledger
|
||||
}
|
||||
}
|
||||
|
||||
func mapManagedWalletEndpoint(endpoint *srequest.ManagedWalletEndpoint) *orchestratorv1.ManagedWalletEndpoint {
|
||||
func mapManagedWalletEndpoint(endpoint *srequest.ManagedWalletEndpoint) (*orchestratorv1.ManagedWalletEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
asset, err := mapAsset(endpoint.Asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &orchestratorv1.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: endpoint.ManagedWalletRef,
|
||||
Asset: mapAsset(endpoint.Asset),
|
||||
}
|
||||
Asset: asset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapExternalChainEndpoint(endpoint *srequest.ExternalChainEndpoint) *orchestratorv1.ExternalChainEndpoint {
|
||||
func mapExternalChainEndpoint(endpoint *srequest.ExternalChainEndpoint) (*orchestratorv1.ExternalChainEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
asset, err := mapAsset(endpoint.Asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &orchestratorv1.ExternalChainEndpoint{
|
||||
Asset: mapAsset(endpoint.Asset),
|
||||
Asset: asset,
|
||||
Address: endpoint.Address,
|
||||
Memo: endpoint.Memo,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapAsset(asset *srequest.Asset) *chainv1.Asset {
|
||||
func mapAsset(asset *srequest.Asset) (*chainv1.Asset, error) {
|
||||
if asset == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
chain, err := mapChainNetwork(asset.Chain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chainv1.Asset{
|
||||
Chain: chainv1.ChainNetwork(asset.Chain),
|
||||
Chain: chain,
|
||||
TokenSymbol: asset.TokenSymbol,
|
||||
ContractAddress: asset.ContractAddress,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapMoney(m *srequest.Money) *moneyv1.Money {
|
||||
@@ -134,9 +177,13 @@ func mapFXIntent(fx *srequest.FXIntent) (*orchestratorv1.FXIntent, error) {
|
||||
if fx == nil {
|
||||
return nil, nil
|
||||
}
|
||||
side, err := mapFXSide(fx.Side)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &orchestratorv1.FXIntent{
|
||||
Pair: mapCurrencyPair(fx.Pair),
|
||||
Side: fxv1.Side(fx.Side),
|
||||
Side: side,
|
||||
Firm: fx.Firm,
|
||||
TtlMs: fx.TTLms,
|
||||
PreferredProvider: fx.PreferredProvider,
|
||||
@@ -154,12 +201,79 @@ func mapCurrencyPair(pair *srequest.CurrencyPair) *fxv1.CurrencyPair {
|
||||
}
|
||||
}
|
||||
|
||||
func mapPolicyOverrides(policy *srequest.PolicyOverrides) *feesv1.PolicyOverrides {
|
||||
if policy == nil {
|
||||
func mapCardEndpoint(card *srequest.CardEndpoint) *orchestratorv1.CardEndpoint {
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
return &feesv1.PolicyOverrides{
|
||||
InsufficientNet: feesv1.InsufficientNetPolicy(policy.InsufficientNet),
|
||||
result := &orchestratorv1.CardEndpoint{
|
||||
CardholderName: strings.TrimSpace(card.Cardholder),
|
||||
ExpMonth: card.ExpMonth,
|
||||
ExpYear: card.ExpYear,
|
||||
Country: strings.TrimSpace(card.Country),
|
||||
MaskedPan: strings.TrimSpace(card.MaskedPan),
|
||||
}
|
||||
if pan := strings.TrimSpace(card.Pan); pan != "" {
|
||||
result.Card = &orchestratorv1.CardEndpoint_Pan{Pan: pan}
|
||||
}
|
||||
if token := strings.TrimSpace(card.Token); token != "" {
|
||||
result.Card = &orchestratorv1.CardEndpoint_Token{Token: token}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapPaymentKind(kind srequest.PaymentKind) (orchestratorv1.PaymentKind, error) {
|
||||
switch strings.TrimSpace(string(kind)) {
|
||||
case "", string(srequest.PaymentKindUnspecified):
|
||||
return orchestratorv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED, nil
|
||||
case string(srequest.PaymentKindPayout):
|
||||
return orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT, nil
|
||||
case string(srequest.PaymentKindInternalTransfer):
|
||||
return orchestratorv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER, nil
|
||||
case string(srequest.PaymentKindFxConversion):
|
||||
return orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION, nil
|
||||
default:
|
||||
return orchestratorv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED, merrors.InvalidArgument("unsupported payment kind: " + string(kind))
|
||||
}
|
||||
}
|
||||
|
||||
func mapSettlementMode(mode srequest.SettlementMode) (orchestratorv1.SettlementMode, error) {
|
||||
switch strings.TrimSpace(string(mode)) {
|
||||
case "", string(srequest.SettlementModeUnspecified):
|
||||
return orchestratorv1.SettlementMode_SETTLEMENT_MODE_UNSPECIFIED, nil
|
||||
case string(srequest.SettlementModeFixSource):
|
||||
return orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_SOURCE, nil
|
||||
case string(srequest.SettlementModeFixReceived):
|
||||
return orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED, nil
|
||||
default:
|
||||
return orchestratorv1.SettlementMode_SETTLEMENT_MODE_UNSPECIFIED, merrors.InvalidArgument("unsupported settlement mode: " + string(mode))
|
||||
}
|
||||
}
|
||||
|
||||
func mapFXSide(side srequest.FXSide) (fxv1.Side, error) {
|
||||
switch strings.TrimSpace(string(side)) {
|
||||
case "", string(srequest.FXSideUnspecified):
|
||||
return fxv1.Side_SIDE_UNSPECIFIED, nil
|
||||
case string(srequest.FXSideBuyBaseSellQuote):
|
||||
return fxv1.Side_BUY_BASE_SELL_QUOTE, nil
|
||||
case string(srequest.FXSideSellBaseBuyQuote):
|
||||
return fxv1.Side_SELL_BASE_BUY_QUOTE, nil
|
||||
default:
|
||||
return fxv1.Side_SIDE_UNSPECIFIED, merrors.InvalidArgument("unsupported fx side: " + string(side))
|
||||
}
|
||||
}
|
||||
|
||||
func mapChainNetwork(chain srequest.ChainNetwork) (chainv1.ChainNetwork, error) {
|
||||
switch strings.TrimSpace(string(chain)) {
|
||||
case "", string(srequest.ChainNetworkUnspecified):
|
||||
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, nil
|
||||
case string(srequest.ChainNetworkEthereumMainnet):
|
||||
return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
|
||||
case string(srequest.ChainNetworkArbitrumOne):
|
||||
return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
|
||||
case string(srequest.ChainNetworkOtherEVM):
|
||||
return chainv1.ChainNetwork_CHAIN_NETWORK_OTHER_EVM, nil
|
||||
default:
|
||||
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("unsupported chain network: " + string(chain))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,16 +39,29 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
if expectQuote && strings.TrimSpace(payload.QuoteRef) == "" {
|
||||
return response.BadPayload(a.logger, a.Name(), merrors.InvalidArgument("quote_ref is required"))
|
||||
}
|
||||
if !expectQuote {
|
||||
payload.QuoteRef = ""
|
||||
|
||||
if expectQuote {
|
||||
if payload.QuoteRef == "" {
|
||||
return response.BadPayload(a.logger, a.Name(), merrors.InvalidArgument("quoteRef is required"))
|
||||
}
|
||||
if payload.Intent != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), merrors.DataConflict("quoteRef cannot be combined with intent"))
|
||||
}
|
||||
} else {
|
||||
if payload.Intent == nil {
|
||||
return response.BadPayload(a.logger, a.Name(), merrors.InvalidArgument("intent is required"))
|
||||
}
|
||||
if payload.QuoteRef != "" {
|
||||
return response.BadPayload(a.logger, a.Name(), merrors.DataConflict("quoteRef cannot be used when intent is provided"))
|
||||
}
|
||||
}
|
||||
|
||||
intent, err := mapPaymentIntent(payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
var intent *orchestratorv1.PaymentIntent
|
||||
if payload.Intent != nil {
|
||||
intent, err = mapPaymentIntent(payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
req := &orchestratorv1.InitiatePaymentRequest{
|
||||
@@ -57,8 +70,6 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
},
|
||||
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
|
||||
Intent: intent,
|
||||
FeeQuoteToken: strings.TrimSpace(payload.FeeQuoteToken),
|
||||
FxQuoteRef: strings.TrimSpace(payload.FxQuoteRef),
|
||||
QuoteRef: strings.TrimSpace(payload.QuoteRef),
|
||||
Metadata: payload.Metadata,
|
||||
}
|
||||
@@ -80,11 +91,10 @@ func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
|
||||
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
||||
}
|
||||
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||
if payload.IdempotencyKey == "" {
|
||||
return nil, merrors.InvalidArgument("idempotencyKey is required")
|
||||
}
|
||||
if payload.Intent == nil {
|
||||
return nil, merrors.InvalidArgument("intent is required")
|
||||
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
|
||||
|
||||
if err := payload.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
intent, err := mapPaymentIntent(payload.Intent)
|
||||
intent, err := mapPaymentIntent(&payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
@@ -50,7 +50,6 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
},
|
||||
IdempotencyKey: payload.IdempotencyKey,
|
||||
Intent: intent,
|
||||
PreviewOnly: payload.PreviewOnly,
|
||||
}
|
||||
|
||||
resp, err := a.client.QuotePayment(ctx, req)
|
||||
@@ -70,11 +69,8 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
|
||||
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
||||
}
|
||||
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
|
||||
if payload.IdempotencyKey == "" {
|
||||
return nil, merrors.InvalidArgument("idempotencyKey is required")
|
||||
}
|
||||
if payload.Intent == nil {
|
||||
return nil, merrors.InvalidArgument("intent is required")
|
||||
if err := payload.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user