payment quotation v2 + payment orchestration v2 draft

This commit is contained in:
Stephan D
2026-02-24 13:01:35 +01:00
parent 0646f55189
commit 6444813f38
289 changed files with 17005 additions and 16065 deletions

View File

@@ -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
}

View File

@@ -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 := &quotationv2.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"`
}

View File

@@ -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, &quotationv2.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)
}

View File

@@ -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) {

View File

@@ -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 := &quotationv1.QuotePaymentRequest{
Meta: &sharedv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
req := &quotationv2.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 := &quotationv1.QuotePaymentsRequest{
Meta: &sharedv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
req := &quotationv2.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)

View File

@@ -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)