334 lines
11 KiB
Go
334 lines
11 KiB
Go
package paymentapiimp
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
pkgmodel "github.com/tech/sendico/pkg/model"
|
|
payecon "github.com/tech/sendico/pkg/payments/economics"
|
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
|
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
|
"github.com/tech/sendico/server/interface/api/srequest"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
)
|
|
|
|
func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, error) {
|
|
if intent == nil {
|
|
return nil, merrors.InvalidArgument("intent is required")
|
|
}
|
|
if err := validatePaymentKind(intent.Kind); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
settlementMode, err := mapSettlementMode(intent.SettlementMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
feeTreatment, err := mapFeeTreatment(intent.FeeTreatment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolvedSettlementMode, resolvedFeeTreatment, err := payecon.ResolveSettlementAndFee(settlementMode, feeTreatment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settlementCurrency := resolveSettlementCurrency(intent)
|
|
if settlementCurrency == "" {
|
|
return nil, merrors.InvalidArgument("unable to derive settlement currency from intent")
|
|
}
|
|
|
|
source, err := mapQuoteEndpoint(intent.Source, "intent.source")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
destination, err := mapQuoteEndpoint(intent.Destination, "intent.destination")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
quoteIntent := "ationv2.QuoteIntent{
|
|
Source: source,
|
|
Destination: destination,
|
|
Amount: mapMoney(intent.Amount),
|
|
SettlementMode: resolvedSettlementMode,
|
|
FeeTreatment: resolvedFeeTreatment,
|
|
SettlementCurrency: settlementCurrency,
|
|
Fx: mapFXIntent(intent),
|
|
}
|
|
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
|
|
quoteIntent.Comment = comment
|
|
}
|
|
return quoteIntent, nil
|
|
}
|
|
|
|
func mapFXIntent(intent *srequest.PaymentIntent) *sharedv1.FXIntent {
|
|
if intent == nil || intent.FX == nil || intent.FX.Pair == nil {
|
|
return nil
|
|
}
|
|
side := fxv1.Side_SIDE_UNSPECIFIED
|
|
switch strings.TrimSpace(string(intent.FX.Side)) {
|
|
case string(srequest.FXSideBuyBaseSellQuote):
|
|
side = fxv1.Side_BUY_BASE_SELL_QUOTE
|
|
case string(srequest.FXSideSellBaseBuyQuote):
|
|
side = fxv1.Side_SELL_BASE_BUY_QUOTE
|
|
}
|
|
if side == fxv1.Side_SIDE_UNSPECIFIED {
|
|
side = fxv1.Side_SELL_BASE_BUY_QUOTE
|
|
}
|
|
return &sharedv1.FXIntent{
|
|
Pair: &fxv1.CurrencyPair{
|
|
Base: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Base)),
|
|
Quote: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Quote)),
|
|
},
|
|
Side: side,
|
|
Firm: intent.FX.Firm,
|
|
TtlMs: intent.FX.TTLms,
|
|
PreferredProvider: strings.TrimSpace(intent.FX.PreferredProvider),
|
|
MaxAgeMs: intent.FX.MaxAgeMs,
|
|
}
|
|
}
|
|
|
|
func validatePaymentKind(kind srequest.PaymentKind) error {
|
|
switch strings.TrimSpace(string(kind)) {
|
|
case string(srequest.PaymentKindPayout), string(srequest.PaymentKindInternalTransfer), string(srequest.PaymentKindFxConversion):
|
|
return nil
|
|
default:
|
|
return merrors.InvalidArgument("unsupported payment kind: " + string(kind))
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return strings.TrimSpace(intent.Amount.Currency)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func mapQuoteEndpoint(endpoint *srequest.Endpoint, field string) (*endpointv1.PaymentEndpoint, error) {
|
|
if endpoint == nil {
|
|
return nil, merrors.InvalidArgument(field + " is required")
|
|
}
|
|
|
|
switch endpoint.Type {
|
|
case srequest.EndpointTypeLedger:
|
|
payload, err := endpoint.DecodeLedger()
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
|
}
|
|
method := &ledgerMethodData{
|
|
LedgerAccountRef: strings.TrimSpace(payload.LedgerAccountRef),
|
|
ContraLedgerAccountRef: strings.TrimSpace(payload.ContraLedgerAccountRef),
|
|
}
|
|
if method.LedgerAccountRef == "" {
|
|
return nil, merrors.InvalidArgument(field + ".ledger_account_ref is required")
|
|
}
|
|
return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, method)
|
|
|
|
case srequest.EndpointTypeManagedWallet:
|
|
payload, err := endpoint.DecodeManagedWallet()
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
|
}
|
|
method := &pkgmodel.WalletPaymentData{WalletID: strings.TrimSpace(payload.ManagedWalletRef)}
|
|
if method.WalletID == "" {
|
|
return nil, merrors.InvalidArgument(field + ".managed_wallet_ref is required")
|
|
}
|
|
return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, method)
|
|
|
|
case srequest.EndpointTypeWallet:
|
|
payload, err := endpoint.DecodeWallet()
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
|
}
|
|
method := &pkgmodel.WalletPaymentData{WalletID: strings.TrimSpace(payload.WalletID)}
|
|
if method.WalletID == "" {
|
|
return nil, merrors.InvalidArgument(field + ".walletId is required")
|
|
}
|
|
return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, method)
|
|
|
|
case srequest.EndpointTypeExternalChain:
|
|
payload, err := endpoint.DecodeExternalChain()
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
|
}
|
|
method, mapErr := mapExternalChainMethod(payload, field)
|
|
if mapErr != nil {
|
|
return nil, mapErr
|
|
}
|
|
return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, method)
|
|
|
|
case srequest.EndpointTypeCard:
|
|
payload, err := endpoint.DecodeCard()
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
|
}
|
|
method := &pkgmodel.CardPaymentData{
|
|
Pan: strings.TrimSpace(payload.Pan),
|
|
FirstName: strings.TrimSpace(payload.FirstName),
|
|
LastName: strings.TrimSpace(payload.LastName),
|
|
ExpMonth: uint32ToString(payload.ExpMonth),
|
|
ExpYear: uint32ToString(payload.ExpYear),
|
|
Country: strings.TrimSpace(payload.Country),
|
|
}
|
|
if method.Pan == "" {
|
|
return nil, merrors.InvalidArgument(field + ".pan is required")
|
|
}
|
|
return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, method)
|
|
|
|
case srequest.EndpointTypeCardToken:
|
|
payload, err := endpoint.DecodeCardToken()
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
|
}
|
|
method := &pkgmodel.TokenPaymentData{
|
|
Token: strings.TrimSpace(payload.Token),
|
|
Last4: strings.TrimSpace(payload.MaskedPan),
|
|
}
|
|
if method.Token == "" {
|
|
return nil, merrors.InvalidArgument(field + ".token is required")
|
|
}
|
|
return endpointFromMethod(endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, method)
|
|
|
|
case "":
|
|
return nil, merrors.InvalidArgument(field + " endpoint type is required")
|
|
|
|
default:
|
|
return nil, merrors.InvalidArgument(field + " endpoint type is unsupported in v2: " + string(endpoint.Type))
|
|
}
|
|
}
|
|
|
|
func mapExternalChainMethod(payload srequest.ExternalChainEndpoint, field string) (*pkgmodel.CryptoAddressPaymentData, error) {
|
|
address := strings.TrimSpace(payload.Address)
|
|
if address == "" {
|
|
return nil, merrors.InvalidArgument(field + ".address is required")
|
|
}
|
|
if payload.Asset == nil {
|
|
return nil, merrors.InvalidArgument(field + ".asset is required")
|
|
}
|
|
token := strings.ToUpper(strings.TrimSpace(payload.Asset.TokenSymbol))
|
|
if token == "" {
|
|
return nil, merrors.InvalidArgument(field + ".asset.token_symbol is required")
|
|
}
|
|
if _, err := mapChainNetwork(payload.Asset.Chain); err != nil {
|
|
return nil, merrors.InvalidArgument(field + ".asset.chain: " + err.Error())
|
|
}
|
|
|
|
result := &pkgmodel.CryptoAddressPaymentData{
|
|
Currency: pkgmodel.Currency(token),
|
|
Address: address,
|
|
Network: strings.ToUpper(strings.TrimSpace(string(payload.Asset.Chain))),
|
|
}
|
|
if memo := strings.TrimSpace(payload.Memo); memo != "" {
|
|
result.DestinationTag = &memo
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func endpointFromMethod(methodType endpointv1.PaymentMethodType, data any) (*endpointv1.PaymentEndpoint, error) {
|
|
raw, err := bson.Marshal(data)
|
|
if err != nil {
|
|
return nil, merrors.InternalWrap(err, "failed to encode payment method data")
|
|
}
|
|
method := &endpointv1.PaymentMethod{
|
|
Type: methodType,
|
|
Data: raw,
|
|
}
|
|
return &endpointv1.PaymentEndpoint{
|
|
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
|
PaymentMethod: method,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func mapMoney(m *paymenttypes.Money) *moneyv1.Money {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
return &moneyv1.Money{
|
|
Amount: m.Amount,
|
|
Currency: m.Currency,
|
|
}
|
|
}
|
|
|
|
func mapSettlementMode(mode srequest.SettlementMode) (paymentv1.SettlementMode, error) {
|
|
switch strings.TrimSpace(string(mode)) {
|
|
case "", string(srequest.SettlementModeUnspecified):
|
|
return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED, nil
|
|
case string(srequest.SettlementModeFixSource):
|
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE, nil
|
|
case string(srequest.SettlementModeFixReceived):
|
|
return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED, nil
|
|
default:
|
|
return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported settlement mode: " + string(mode))
|
|
}
|
|
}
|
|
|
|
func mapFeeTreatment(treatment srequest.FeeTreatment) (quotationv2.FeeTreatment, error) {
|
|
switch strings.TrimSpace(string(treatment)) {
|
|
case "", string(srequest.FeeTreatmentUnspecified):
|
|
return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, nil
|
|
case string(srequest.FeeTreatmentAddToSource):
|
|
return quotationv2.FeeTreatment_FEE_TREATMENT_ADD_TO_SOURCE, nil
|
|
case string(srequest.FeeTreatmentDeductFromDestination):
|
|
return quotationv2.FeeTreatment_FEE_TREATMENT_DEDUCT_FROM_DESTINATION, nil
|
|
default:
|
|
return quotationv2.FeeTreatment_FEE_TREATMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported fee treatment: " + string(treatment))
|
|
}
|
|
}
|
|
|
|
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.ChainNetworkTronMainnet):
|
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
|
|
case string(srequest.ChainNetworkTronNile):
|
|
return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE, nil
|
|
default:
|
|
return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("unsupported chain network: " + string(chain))
|
|
}
|
|
}
|
|
|
|
func uint32ToString(v uint32) string {
|
|
if v == 0 {
|
|
return ""
|
|
}
|
|
return strconv.FormatUint(uint64(v), 10)
|
|
}
|
|
|
|
type ledgerMethodData struct {
|
|
LedgerAccountRef string `bson:"ledgerAccountRef"`
|
|
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
|
|
}
|