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"` }