Files
sendico/api/edge/bff/internal/server/paymentapiimp/mapper.go
2026-03-05 20:27:45 +01:00

332 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 := &quotationv2.QuoteIntent{
Source: source,
Destination: destination,
Amount: mapMoney(intent.Amount),
SettlementMode: resolvedSettlementMode,
FeeTreatment: resolvedFeeTreatment,
SettlementCurrency: settlementCurrency,
Fx: mapFXIntent(intent),
Comment: strings.TrimSpace(intent.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"`
}