payment quotation v2 + payment orchestration v2 draft
This commit is contained in:
@@ -4,18 +4,19 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const maxInt32 = int64(1<<31 - 1)
|
||||
@@ -38,9 +39,7 @@ func (a *PaymentAPI) listPayments(r *http.Request, account *model.Account, token
|
||||
return response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
|
||||
}
|
||||
|
||||
req := &orchestratorv1.ListPaymentsRequest{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
}
|
||||
req := &orchestrationv2.ListPaymentsRequest{Meta: requestMeta(orgRef.Hex(), "")}
|
||||
|
||||
if page, err := listPaymentsPage(r); err != nil {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
@@ -49,17 +48,33 @@ func (a *PaymentAPI) listPayments(r *http.Request, account *model.Account, token
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
if sourceRef := strings.TrimSpace(query.Get("source_ref")); sourceRef != "" {
|
||||
req.SourceRef = sourceRef
|
||||
if quotationRef := firstNonEmpty(query.Get("quotation_ref"), query.Get("quote_ref")); quotationRef != "" {
|
||||
req.QuotationRef = quotationRef
|
||||
}
|
||||
if destinationRef := strings.TrimSpace(query.Get("destination_ref")); destinationRef != "" {
|
||||
req.DestinationRef = destinationRef
|
||||
createdFrom, err := parseRFC3339Timestamp(firstNonEmpty(query.Get("created_from"), query.Get("createdFrom")), "created_from")
|
||||
if err != nil {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
if createdFrom != nil {
|
||||
req.CreatedFrom = createdFrom
|
||||
}
|
||||
createdTo, err := parseRFC3339Timestamp(firstNonEmpty(query.Get("created_to"), query.Get("createdTo")), "created_to")
|
||||
if err != nil {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
if createdTo != nil {
|
||||
req.CreatedTo = createdTo
|
||||
}
|
||||
if req.GetCreatedFrom() != nil && req.GetCreatedTo() != nil {
|
||||
if !req.GetCreatedTo().AsTime().After(req.GetCreatedFrom().AsTime()) {
|
||||
return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("created_to must be after created_from", "created_to"))
|
||||
}
|
||||
}
|
||||
|
||||
if states, err := parsePaymentStateFilters(r); err != nil {
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
} else if len(states) > 0 {
|
||||
req.FilterStates = states
|
||||
req.States = states
|
||||
}
|
||||
|
||||
resp, err := a.execution.ListPayments(ctx, req)
|
||||
@@ -106,7 +121,7 @@ func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error)
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func parsePaymentStateFilters(r *http.Request) ([]sharedv1.PaymentState, error) {
|
||||
func parsePaymentStateFilters(r *http.Request) ([]orchestrationv2.OrchestrationState, error) {
|
||||
query := r.URL.Query()
|
||||
values := append([]string{}, query["state"]...)
|
||||
values = append(values, query["states"]...)
|
||||
@@ -115,14 +130,14 @@ func parsePaymentStateFilters(r *http.Request) ([]sharedv1.PaymentState, error)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
states := make([]sharedv1.PaymentState, 0, len(values))
|
||||
states := make([]orchestrationv2.OrchestrationState, 0, len(values))
|
||||
for _, raw := range values {
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
state, ok := paymentStateFromString(trimmed)
|
||||
state, ok := orchestrationStateFromString(trimmed)
|
||||
if !ok {
|
||||
return nil, merrors.InvalidArgument("unsupported payment state: "+trimmed, "state")
|
||||
}
|
||||
@@ -136,17 +151,49 @@ func parsePaymentStateFilters(r *http.Request) ([]sharedv1.PaymentState, error)
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func paymentStateFromString(value string) (sharedv1.PaymentState, bool) {
|
||||
func orchestrationStateFromString(value string) (orchestrationv2.OrchestrationState, bool) {
|
||||
upper := strings.ToUpper(strings.TrimSpace(value))
|
||||
if upper == "" {
|
||||
return 0, false
|
||||
}
|
||||
if !strings.HasPrefix(upper, "PAYMENT_STATE_") {
|
||||
upper = "PAYMENT_STATE_" + upper
|
||||
switch upper {
|
||||
case "PAYMENT_STATE_ACCEPTED", "ACCEPTED":
|
||||
return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED, true
|
||||
case "PAYMENT_STATE_FUNDS_RESERVED", "FUNDS_RESERVED", "PAYMENT_STATE_SUBMITTED", "SUBMITTED":
|
||||
return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_EXECUTING, true
|
||||
case "PAYMENT_STATE_SETTLED", "SETTLED":
|
||||
return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_SETTLED, true
|
||||
case "PAYMENT_STATE_FAILED", "FAILED", "PAYMENT_STATE_CANCELLED", "CANCELLED":
|
||||
return orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_FAILED, true
|
||||
}
|
||||
enumValue, ok := sharedv1.PaymentState_value[upper]
|
||||
if !strings.HasPrefix(upper, "ORCHESTRATION_STATE_") {
|
||||
upper = "ORCHESTRATION_STATE_" + upper
|
||||
}
|
||||
enumValue, ok := orchestrationv2.OrchestrationState_value[upper]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return sharedv1.PaymentState(enumValue), true
|
||||
return orchestrationv2.OrchestrationState(enumValue), true
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, trimmed)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid "+field+", expected RFC3339", field)
|
||||
}
|
||||
return timestamppb.New(parsed), nil
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package paymentapiimp
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pkgmodel "github.com/tech/sendico/pkg/model"
|
||||
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"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func mapPaymentIntent(intent *srequest.PaymentIntent) (*sharedv1.PaymentIntent, error) {
|
||||
func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, error) {
|
||||
if intent == nil {
|
||||
return nil, merrors.InvalidArgument("intent is required")
|
||||
}
|
||||
|
||||
kind, err := mapPaymentKind(intent.Kind)
|
||||
if err != nil {
|
||||
if err := validatePaymentKind(intent.Kind); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -33,33 +33,35 @@ func mapPaymentIntent(intent *srequest.PaymentIntent) (*sharedv1.PaymentIntent,
|
||||
settlementCurrency = resolveSettlementCurrency(intent)
|
||||
}
|
||||
|
||||
source, err := mapPaymentEndpoint(intent.Source, "source")
|
||||
source, err := mapQuoteEndpoint(intent.Source, "intent.source")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destination, err := mapPaymentEndpoint(intent.Destination, "destination")
|
||||
destination, err := mapQuoteEndpoint(intent.Destination, "intent.destination")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fx, err := mapFXIntent(intent.FX)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sharedv1.PaymentIntent{
|
||||
Ref: uuid.New().String(),
|
||||
Kind: kind,
|
||||
quoteIntent := "ationv2.QuoteIntent{
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
Amount: mapMoney(intent.Amount),
|
||||
RequiresFx: fx != nil,
|
||||
Fx: fx,
|
||||
SettlementMode: settlementMode,
|
||||
SettlementCurrency: settlementCurrency,
|
||||
Attributes: copyStringMap(intent.Attributes),
|
||||
Customer: mapCustomer(intent.Customer),
|
||||
}, nil
|
||||
}
|
||||
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
|
||||
quoteIntent.Comment = comment
|
||||
}
|
||||
return quoteIntent, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -81,150 +83,147 @@ func resolveSettlementCurrency(intent *srequest.PaymentIntent) string {
|
||||
return quote
|
||||
}
|
||||
}
|
||||
if intent.Amount != nil {
|
||||
amountCurrency := strings.TrimSpace(intent.Amount.Currency)
|
||||
if amountCurrency != "" {
|
||||
switch {
|
||||
case strings.EqualFold(amountCurrency, base) && quote != "":
|
||||
return quote
|
||||
case strings.EqualFold(amountCurrency, quote) && base != "":
|
||||
return base
|
||||
default:
|
||||
return amountCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
if quote != "" {
|
||||
return quote
|
||||
}
|
||||
if base != "" {
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
if intent.Amount != nil {
|
||||
return strings.TrimSpace(intent.Amount.Currency)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func mapPaymentEndpoint(endpoint *srequest.Endpoint, field string) (*sharedv1.PaymentEndpoint, error) {
|
||||
func mapQuoteEndpoint(endpoint *srequest.Endpoint, field string) (*endpointv1.PaymentEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil, nil
|
||||
return nil, merrors.InvalidArgument(field + " is required")
|
||||
}
|
||||
|
||||
var result sharedv1.PaymentEndpoint
|
||||
switch endpoint.Type {
|
||||
case srequest.EndpointTypeLedger:
|
||||
payload, err := endpoint.DecodeLedger()
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
||||
}
|
||||
result.Endpoint = &sharedv1.PaymentEndpoint_Ledger{
|
||||
Ledger: mapLedgerEndpoint(&payload),
|
||||
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 + " endpoint: " + err.Error())
|
||||
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
||||
}
|
||||
mw, err := mapManagedWalletEndpoint(&payload)
|
||||
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 + " endpoint: " + err.Error())
|
||||
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
||||
}
|
||||
result.Endpoint = &sharedv1.PaymentEndpoint_ManagedWallet{
|
||||
ManagedWallet: mw,
|
||||
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 + " endpoint: " + err.Error())
|
||||
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
||||
}
|
||||
ext, err := mapExternalChainEndpoint(&payload)
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument(field + " endpoint: " + err.Error())
|
||||
}
|
||||
result.Endpoint = &sharedv1.PaymentEndpoint_ExternalChain{
|
||||
ExternalChain: ext,
|
||||
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 + " endpoint: " + err.Error())
|
||||
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
||||
}
|
||||
result.Endpoint = &sharedv1.PaymentEndpoint_Card{
|
||||
Card: mapCardEndpoint(&payload),
|
||||
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 + " endpoint: " + err.Error())
|
||||
return nil, merrors.InvalidArgument(field + ": " + err.Error())
|
||||
}
|
||||
result.Endpoint = &sharedv1.PaymentEndpoint_Card{
|
||||
Card: mapCardTokenEndpoint(&payload),
|
||||
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 has unsupported type: " + string(endpoint.Type))
|
||||
}
|
||||
|
||||
result.Metadata = copyStringMap(endpoint.Metadata)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func mapLedgerEndpoint(endpoint *srequest.LedgerEndpoint) *sharedv1.LedgerEndpoint {
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedv1.LedgerEndpoint{
|
||||
LedgerAccountRef: endpoint.LedgerAccountRef,
|
||||
ContraLedgerAccountRef: endpoint.ContraLedgerAccountRef,
|
||||
return nil, merrors.InvalidArgument(field + " endpoint type is unsupported in v2: " + string(endpoint.Type))
|
||||
}
|
||||
}
|
||||
|
||||
func mapManagedWalletEndpoint(endpoint *srequest.ManagedWalletEndpoint) (*sharedv1.ManagedWalletEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil, nil
|
||||
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")
|
||||
}
|
||||
asset, err := mapAsset(endpoint.Asset)
|
||||
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, err
|
||||
return nil, merrors.InternalWrap(err, "failed to encode payment method data")
|
||||
}
|
||||
return &sharedv1.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: endpoint.ManagedWalletRef,
|
||||
Asset: asset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapExternalChainEndpoint(endpoint *srequest.ExternalChainEndpoint) (*sharedv1.ExternalChainEndpoint, error) {
|
||||
if endpoint == nil {
|
||||
return nil, nil
|
||||
method := &endpointv1.PaymentMethod{
|
||||
Type: methodType,
|
||||
Data: raw,
|
||||
}
|
||||
asset, err := mapAsset(endpoint.Asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedv1.ExternalChainEndpoint{
|
||||
Asset: asset,
|
||||
Address: endpoint.Address,
|
||||
Memo: endpoint.Memo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapAsset(asset *srequest.Asset) (*chainv1.Asset, error) {
|
||||
if asset == nil {
|
||||
return nil, nil
|
||||
}
|
||||
chain, err := mapChainNetwork(asset.Chain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chainv1.Asset{
|
||||
Chain: chain,
|
||||
TokenSymbol: asset.TokenSymbol,
|
||||
ContractAddress: asset.ContractAddress,
|
||||
return &endpointv1.PaymentEndpoint{
|
||||
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
|
||||
PaymentMethod: method,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -238,94 +237,6 @@ func mapMoney(m *paymenttypes.Money) *moneyv1.Money {
|
||||
}
|
||||
}
|
||||
|
||||
func mapFXIntent(fx *srequest.FXIntent) (*sharedv1.FXIntent, error) {
|
||||
if fx == nil {
|
||||
return nil, nil
|
||||
}
|
||||
side, err := mapFXSide(fx.Side)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedv1.FXIntent{
|
||||
Pair: mapCurrencyPair(fx.Pair),
|
||||
Side: side,
|
||||
Firm: fx.Firm,
|
||||
TtlMs: fx.TTLms,
|
||||
PreferredProvider: fx.PreferredProvider,
|
||||
MaxAgeMs: fx.MaxAgeMs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mapCustomer(customer *srequest.Customer) *sharedv1.Customer {
|
||||
if customer == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedv1.Customer{
|
||||
Id: strings.TrimSpace(customer.ID),
|
||||
FirstName: strings.TrimSpace(customer.FirstName),
|
||||
MiddleName: strings.TrimSpace(customer.MiddleName),
|
||||
LastName: strings.TrimSpace(customer.LastName),
|
||||
Ip: strings.TrimSpace(customer.IP),
|
||||
Zip: strings.TrimSpace(customer.Zip),
|
||||
Country: strings.TrimSpace(customer.Country),
|
||||
State: strings.TrimSpace(customer.State),
|
||||
City: strings.TrimSpace(customer.City),
|
||||
Address: strings.TrimSpace(customer.Address),
|
||||
}
|
||||
}
|
||||
|
||||
func mapCurrencyPair(pair *srequest.CurrencyPair) *fxv1.CurrencyPair {
|
||||
if pair == nil {
|
||||
return nil
|
||||
}
|
||||
return &fxv1.CurrencyPair{
|
||||
Base: pair.Base,
|
||||
Quote: pair.Quote,
|
||||
}
|
||||
}
|
||||
|
||||
func mapCardEndpoint(card *srequest.CardEndpoint) *sharedv1.CardEndpoint {
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
result := &sharedv1.CardEndpoint{
|
||||
CardholderName: strings.TrimSpace(card.FirstName),
|
||||
CardholderSurname: strings.TrimSpace(card.LastName),
|
||||
ExpMonth: card.ExpMonth,
|
||||
ExpYear: card.ExpYear,
|
||||
Country: strings.TrimSpace(card.Country),
|
||||
}
|
||||
if pan := strings.TrimSpace(card.Pan); pan != "" {
|
||||
result.Card = &sharedv1.CardEndpoint_Pan{Pan: pan}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mapCardTokenEndpoint(card *srequest.CardTokenEndpoint) *sharedv1.CardEndpoint {
|
||||
if card == nil {
|
||||
return nil
|
||||
}
|
||||
return &sharedv1.CardEndpoint{
|
||||
Card: &sharedv1.CardEndpoint_Token{Token: strings.TrimSpace(card.Token)},
|
||||
MaskedPan: strings.TrimSpace(card.MaskedPan),
|
||||
}
|
||||
}
|
||||
|
||||
func mapPaymentKind(kind srequest.PaymentKind) (sharedv1.PaymentKind, error) {
|
||||
switch strings.TrimSpace(string(kind)) {
|
||||
case "", string(srequest.PaymentKindUnspecified):
|
||||
return sharedv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED, nil
|
||||
case string(srequest.PaymentKindPayout):
|
||||
return sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT, nil
|
||||
case string(srequest.PaymentKindInternalTransfer):
|
||||
return sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER, nil
|
||||
case string(srequest.PaymentKindFxConversion):
|
||||
return sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION, nil
|
||||
default:
|
||||
return sharedv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED, merrors.InvalidArgument("unsupported payment kind: " + string(kind))
|
||||
}
|
||||
}
|
||||
|
||||
func mapSettlementMode(mode srequest.SettlementMode) (paymentv1.SettlementMode, error) {
|
||||
switch strings.TrimSpace(string(mode)) {
|
||||
case "", string(srequest.SettlementModeUnspecified):
|
||||
@@ -339,19 +250,6 @@ func mapSettlementMode(mode srequest.SettlementMode) (paymentv1.SettlementMode,
|
||||
}
|
||||
}
|
||||
|
||||
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):
|
||||
@@ -369,13 +267,14 @@ func mapChainNetwork(chain srequest.ChainNetwork) (chainv1.ChainNetwork, error)
|
||||
}
|
||||
}
|
||||
|
||||
func copyStringMap(src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
func uint32ToString(v uint32) string {
|
||||
if v == 0 {
|
||||
return ""
|
||||
}
|
||||
dst := make(map[string]string, len(src))
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
return dst
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
}
|
||||
|
||||
type ledgerMethodData struct {
|
||||
LedgerAccountRef string `bson:"ledgerAccountRef"`
|
||||
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
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"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
@@ -58,26 +60,41 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
||||
}
|
||||
}
|
||||
|
||||
var intent *sharedv1.PaymentIntent
|
||||
quotationRef := strings.TrimSpace(payload.QuoteRef)
|
||||
intentRef := metadataValue(payload.Metadata, "intent_ref")
|
||||
if payload.Intent != nil {
|
||||
applyCustomerIP(payload.Intent, r.RemoteAddr)
|
||||
intent, err = mapPaymentIntent(payload.Intent)
|
||||
intent, err := mapQuoteIntent(payload.Intent)
|
||||
if err != nil {
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
quoteResp, qErr := a.quotation.QuotePayment(ctx, "ationv2.QuotePaymentRequest{
|
||||
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
|
||||
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
|
||||
Intent: intent,
|
||||
InitiatorRef: initiatorRef(account),
|
||||
})
|
||||
if qErr != nil {
|
||||
a.logger.Warn("Failed to quote payment before execution", zap.Error(qErr), mzap.ObjRef("organization_ref", orgRef))
|
||||
return response.Auto(a.logger, a.Name(), qErr)
|
||||
}
|
||||
quotationRef = strings.TrimSpace(quoteResp.GetQuote().GetQuoteRef())
|
||||
if quotationRef == "" {
|
||||
return response.Auto(a.logger, a.Name(), merrors.DataConflict("quotation service returned empty quote_ref"))
|
||||
}
|
||||
if derived := strings.TrimSpace(quoteResp.GetQuote().GetIntentRef()); derived != "" {
|
||||
intentRef = derived
|
||||
}
|
||||
}
|
||||
|
||||
req := &orchestratorv1.InitiatePaymentRequest{
|
||||
Meta: &sharedv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
|
||||
Intent: intent,
|
||||
QuoteRef: strings.TrimSpace(payload.QuoteRef),
|
||||
Metadata: payload.Metadata,
|
||||
req := &orchestrationv2.ExecutePaymentRequest{
|
||||
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
|
||||
QuotationRef: quotationRef,
|
||||
IntentRef: intentRef,
|
||||
ClientPaymentRef: metadataValue(payload.Metadata, "client_payment_ref"),
|
||||
}
|
||||
|
||||
resp, err := a.execution.InitiatePayment(ctx, req)
|
||||
resp, err := a.execution.ExecutePayment(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to initiate payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
@@ -101,3 +118,29 @@ func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func requestMeta(organizationRef string, idempotencyKey string) *sharedv1.RequestMeta {
|
||||
return &sharedv1.RequestMeta{
|
||||
OrganizationRef: strings.TrimSpace(organizationRef),
|
||||
Trace: &tracev1.TraceContext{
|
||||
IdempotencyKey: strings.TrimSpace(idempotencyKey),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func metadataValue(meta map[string]string, key string) string {
|
||||
if len(meta) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(meta[strings.TrimSpace(key)])
|
||||
}
|
||||
|
||||
func initiatorRef(account *model.Account) string {
|
||||
if account == nil {
|
||||
return ""
|
||||
}
|
||||
if account.ID != bson.NilObjectID {
|
||||
return account.ID.Hex()
|
||||
}
|
||||
return strings.TrimSpace(account.Login)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
@@ -40,22 +39,24 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
req := &orchestratorv1.InitiatePaymentsRequest{
|
||||
Meta: &sharedv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
|
||||
QuoteRef: strings.TrimSpace(payload.QuoteRef),
|
||||
Metadata: payload.Metadata,
|
||||
req := &orchestrationv2.ExecutePaymentRequest{
|
||||
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
|
||||
QuotationRef: strings.TrimSpace(payload.QuoteRef),
|
||||
IntentRef: metadataValue(payload.Metadata, "intent_ref"),
|
||||
ClientPaymentRef: metadataValue(payload.Metadata, "client_payment_ref"),
|
||||
}
|
||||
|
||||
resp, err := a.execution.InitiatePayments(ctx, req)
|
||||
resp, err := a.execution.ExecutePayment(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
return sresponse.PaymentsResponse(a.logger, resp.GetPayments(), token)
|
||||
payments := make([]*orchestrationv2.Payment, 0, 1)
|
||||
if payment := resp.GetPayment(); payment != nil {
|
||||
payments = append(payments, payment)
|
||||
}
|
||||
return sresponse.PaymentsResponse(a.logger, payments, token)
|
||||
}
|
||||
|
||||
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
|
||||
|
||||
@@ -8,8 +8,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
@@ -46,18 +45,18 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
||||
}
|
||||
|
||||
applyCustomerIP(&payload.Intent, r.RemoteAddr)
|
||||
intent, err := mapPaymentIntent(&payload.Intent)
|
||||
intent, err := mapQuoteIntent(&payload.Intent)
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
req := "ationv1.QuotePaymentRequest{
|
||||
Meta: &sharedv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
req := "ationv2.QuotePaymentRequest{
|
||||
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
|
||||
IdempotencyKey: payload.IdempotencyKey,
|
||||
Intent: intent,
|
||||
PreviewOnly: payload.PreviewOnly,
|
||||
InitiatorRef: initiatorRef(account),
|
||||
}
|
||||
|
||||
resp, err := a.quotation.QuotePayment(ctx, req)
|
||||
@@ -97,10 +96,10 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
||||
return response.Auto(a.logger, a.Name(), err)
|
||||
}
|
||||
|
||||
intents := make([]*sharedv1.PaymentIntent, 0, len(payload.Intents))
|
||||
intents := make([]*quotationv2.QuoteIntent, 0, len(payload.Intents))
|
||||
for i := range payload.Intents {
|
||||
applyCustomerIP(&payload.Intents[i], r.RemoteAddr)
|
||||
intent, err := mapPaymentIntent(&payload.Intents[i])
|
||||
intent, err := mapQuoteIntent(&payload.Intents[i])
|
||||
if err != nil {
|
||||
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
|
||||
return response.BadPayload(a.logger, a.Name(), err)
|
||||
@@ -108,13 +107,12 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
||||
intents = append(intents, intent)
|
||||
}
|
||||
|
||||
req := "ationv1.QuotePaymentsRequest{
|
||||
Meta: &sharedv1.RequestMeta{
|
||||
OrganizationRef: orgRef.Hex(),
|
||||
},
|
||||
req := "ationv2.QuotePaymentsRequest{
|
||||
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
|
||||
IdempotencyKey: payload.IdempotencyKey,
|
||||
Intents: intents,
|
||||
PreviewOnly: payload.PreviewOnly,
|
||||
InitiatorRef: initiatorRef(account),
|
||||
}
|
||||
|
||||
resp, err := a.quotation.QuotePayments(ctx, req)
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
msgconsumer "github.com/tech/sendico/pkg/messaging/consumer"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
eapi "github.com/tech/sendico/server/interface/api"
|
||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -30,15 +30,14 @@ import (
|
||||
)
|
||||
|
||||
type executionClient interface {
|
||||
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
|
||||
ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
|
||||
ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type quotationClient interface {
|
||||
QuotePayment(ctx context.Context, req *quotationv1.QuotePaymentRequest) (*quotationv1.QuotePaymentResponse, error)
|
||||
QuotePayments(ctx context.Context, req *quotationv1.QuotePaymentsRequest) (*quotationv1.QuotePaymentsResponse, error)
|
||||
QuotePayment(ctx context.Context, req *quotationv2.QuotePaymentRequest) (*quotationv2.QuotePaymentResponse, error)
|
||||
QuotePayments(ctx context.Context, req *quotationv2.QuotePaymentsRequest) (*quotationv2.QuotePaymentsResponse, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -203,7 +202,7 @@ func (c *quotationClientConfig) setDefaults() {
|
||||
|
||||
type grpcQuotationClient struct {
|
||||
conn *grpc.ClientConn
|
||||
client quotationv1.QuotationServiceClient
|
||||
client quotationv2.QuotationServiceClient
|
||||
callTimeout time.Duration
|
||||
}
|
||||
|
||||
@@ -230,7 +229,7 @@ func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...
|
||||
}
|
||||
return &grpcQuotationClient{
|
||||
conn: conn,
|
||||
client: quotationv1.NewQuotationServiceClient(conn),
|
||||
client: quotationv2.NewQuotationServiceClient(conn),
|
||||
callTimeout: cfg.CallTimeout,
|
||||
}, nil
|
||||
}
|
||||
@@ -242,13 +241,13 @@ func (c *grpcQuotationClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *grpcQuotationClient) QuotePayment(ctx context.Context, req *quotationv1.QuotePaymentRequest) (*quotationv1.QuotePaymentResponse, error) {
|
||||
func (c *grpcQuotationClient) QuotePayment(ctx context.Context, req *quotationv2.QuotePaymentRequest) (*quotationv2.QuotePaymentResponse, error) {
|
||||
callCtx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.QuotePayment(callCtx, req)
|
||||
}
|
||||
|
||||
func (c *grpcQuotationClient) QuotePayments(ctx context.Context, req *quotationv1.QuotePaymentsRequest) (*quotationv1.QuotePaymentsResponse, error) {
|
||||
func (c *grpcQuotationClient) QuotePayments(ctx context.Context, req *quotationv2.QuotePaymentsRequest) (*quotationv2.QuotePaymentsResponse, error) {
|
||||
callCtx, cancel := c.callContext(ctx)
|
||||
defer cancel()
|
||||
return c.client.QuotePayments(callCtx, req)
|
||||
|
||||
Reference in New Issue
Block a user