561 lines
17 KiB
Go
561 lines
17 KiB
Go
package sresponse
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/pkg/api/http/response"
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
|
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
|
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
|
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
|
|
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
type FeeLine struct {
|
|
LedgerAccountRef string `json:"ledgerAccountRef,omitempty"`
|
|
Amount *paymenttypes.Money `json:"amount,omitempty"`
|
|
LineType string `json:"lineType,omitempty"`
|
|
Side string `json:"side,omitempty"`
|
|
Meta map[string]string `json:"meta,omitempty"`
|
|
}
|
|
|
|
type FxQuote struct {
|
|
QuoteRef string `json:"quoteRef,omitempty"`
|
|
BaseCurrency string `json:"baseCurrency,omitempty"`
|
|
QuoteCurrency string `json:"quoteCurrency,omitempty"`
|
|
Side string `json:"side,omitempty"`
|
|
Price string `json:"price,omitempty"`
|
|
BaseAmount *paymenttypes.Money `json:"baseAmount,omitempty"`
|
|
QuoteAmount *paymenttypes.Money `json:"quoteAmount,omitempty"`
|
|
ExpiresAtUnixMs int64 `json:"expiresAtUnixMs,omitempty"`
|
|
PricedAtUnixMs int64 `json:"pricedAtUnixMs,omitempty"`
|
|
Provider string `json:"provider,omitempty"`
|
|
RateRef string `json:"rateRef,omitempty"`
|
|
Firm bool `json:"firm,omitempty"`
|
|
}
|
|
|
|
type PaymentQuote struct {
|
|
QuoteRef string `json:"quoteRef,omitempty"`
|
|
IntentRef string `json:"intentRef,omitempty"`
|
|
Amounts *QuoteAmounts `json:"amounts,omitempty"`
|
|
Fees *QuoteFees `json:"fees,omitempty"`
|
|
FxQuote *FxQuote `json:"fxQuote,omitempty"`
|
|
}
|
|
|
|
type QuoteAmounts struct {
|
|
SourcePrincipal *paymenttypes.Money `json:"sourcePrincipal,omitempty"`
|
|
SourceDebitTotal *paymenttypes.Money `json:"sourceDebitTotal,omitempty"`
|
|
DestinationSettlement *paymenttypes.Money `json:"destinationSettlement,omitempty"`
|
|
}
|
|
|
|
type QuoteFees struct {
|
|
Lines []FeeLine `json:"lines,omitempty"`
|
|
}
|
|
|
|
type PaymentQuotes struct {
|
|
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
|
QuoteRef string `json:"quoteRef,omitempty"`
|
|
Items []PaymentQuote `json:"items,omitempty"`
|
|
}
|
|
|
|
type Payment struct {
|
|
PaymentRef string `json:"paymentRef,omitempty"`
|
|
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
FailureCode string `json:"failureCode,omitempty"`
|
|
FailureReason string `json:"failureReason,omitempty"`
|
|
Operations []PaymentOperation `json:"operations,omitempty"`
|
|
LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
|
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
|
Meta map[string]string `json:"meta,omitempty"`
|
|
}
|
|
|
|
type PaymentOperation struct {
|
|
StepRef string `json:"stepRef,omitempty"`
|
|
Code string `json:"code,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
Amount *paymenttypes.Money `json:"amount,omitempty"`
|
|
ConvertedAmount *paymenttypes.Money `json:"convertedAmount,omitempty"`
|
|
OperationRef string `json:"operationRef,omitempty"`
|
|
Gateway string `json:"gateway,omitempty"`
|
|
FailureCode string `json:"failureCode,omitempty"`
|
|
FailureReason string `json:"failureReason,omitempty"`
|
|
StartedAt time.Time `json:"startedAt,omitempty"`
|
|
CompletedAt time.Time `json:"completedAt,omitempty"`
|
|
}
|
|
|
|
type paymentQuoteResponse struct {
|
|
authResponse `json:",inline"`
|
|
IdempotencyKey string `json:"idempotencyKey,omitempty"`
|
|
Quote *PaymentQuote `json:"quote"`
|
|
}
|
|
|
|
type paymentQuotesResponse struct {
|
|
authResponse `json:",inline"`
|
|
Quote *PaymentQuotes `json:"quote"`
|
|
}
|
|
|
|
type paymentsResponse struct {
|
|
authResponse `json:",inline"`
|
|
Payments []Payment `json:"payments"`
|
|
Page *paginationv1.CursorPageResponse `json:"page,omitempty"`
|
|
}
|
|
|
|
type paymentResponse struct {
|
|
authResponse `json:",inline"`
|
|
Payment *Payment `json:"payment"`
|
|
}
|
|
|
|
// PaymentQuote wraps a payment quote with refreshed access token.
|
|
func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *quotationv2.PaymentQuote, token *TokenData) http.HandlerFunc {
|
|
return response.Ok(logger, paymentQuoteResponse{
|
|
Quote: toPaymentQuote(quote),
|
|
IdempotencyKey: idempotencyKey,
|
|
authResponse: authResponse{AccessToken: *token},
|
|
})
|
|
}
|
|
|
|
// PaymentQuotes wraps batch quotes with refreshed access token.
|
|
func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv2.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
|
|
return response.Ok(logger, paymentQuotesResponse{
|
|
Quote: toPaymentQuotes(resp),
|
|
authResponse: authResponse{AccessToken: *token},
|
|
})
|
|
}
|
|
|
|
// Payments wraps a list of payments with refreshed access token.
|
|
func PaymentsResponse(logger mlogger.Logger, payments []*orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
|
|
return response.Ok(logger, paymentsResponse{
|
|
Payments: toPayments(payments),
|
|
authResponse: authResponse{AccessToken: *token},
|
|
})
|
|
}
|
|
|
|
// PaymentsList wraps a list of payments with refreshed access token and pagination data.
|
|
func PaymentsListResponse(logger mlogger.Logger, resp *orchestrationv2.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
|
|
return response.Ok(logger, paymentsResponse{
|
|
Payments: toPayments(resp.GetPayments()),
|
|
Page: resp.GetPage(),
|
|
authResponse: authResponse{AccessToken: *token},
|
|
})
|
|
}
|
|
|
|
// Payment wraps a payment with refreshed access token.
|
|
func PaymentResponse(logger mlogger.Logger, payment *orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
|
|
return response.Ok(logger, paymentResponse{
|
|
Payment: toPayment(payment),
|
|
authResponse: authResponse{AccessToken: *token},
|
|
})
|
|
}
|
|
|
|
func toFeeLines(lines []*feesv1.DerivedPostingLine) []FeeLine {
|
|
if len(lines) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]FeeLine, 0, len(lines))
|
|
for _, line := range lines {
|
|
if line == nil {
|
|
continue
|
|
}
|
|
result = append(result, FeeLine{
|
|
LedgerAccountRef: line.GetLedgerAccountRef(),
|
|
Amount: toMoney(line.GetMoney()),
|
|
LineType: enumJSONName(line.GetLineType().String()),
|
|
Side: enumJSONName(line.GetSide().String()),
|
|
Meta: line.GetMeta(),
|
|
})
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func toFxQuote(q *oraclev1.Quote) *FxQuote {
|
|
if q == nil {
|
|
return nil
|
|
}
|
|
pair := q.GetPair()
|
|
pricedAtUnixMs := int64(0)
|
|
if ts := q.GetPricedAt(); ts != nil {
|
|
pricedAtUnixMs = ts.AsTime().UnixMilli()
|
|
}
|
|
base := ""
|
|
quote := ""
|
|
if pair != nil {
|
|
base = pair.GetBase()
|
|
quote = pair.GetQuote()
|
|
}
|
|
return &FxQuote{
|
|
QuoteRef: q.GetQuoteRef(),
|
|
BaseCurrency: base,
|
|
QuoteCurrency: quote,
|
|
Side: enumJSONName(q.GetSide().String()),
|
|
Price: q.GetPrice().GetValue(),
|
|
BaseAmount: toMoney(q.GetBaseAmount()),
|
|
QuoteAmount: toMoney(q.GetQuoteAmount()),
|
|
ExpiresAtUnixMs: q.GetExpiresAtUnixMs(),
|
|
PricedAtUnixMs: pricedAtUnixMs,
|
|
Provider: q.GetProvider(),
|
|
RateRef: q.GetRateRef(),
|
|
Firm: q.GetFirm(),
|
|
}
|
|
}
|
|
|
|
func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote {
|
|
if q == nil {
|
|
return nil
|
|
}
|
|
amounts := toQuoteAmounts(q)
|
|
fees := toQuoteFees(q.GetFeeLines())
|
|
return &PaymentQuote{
|
|
QuoteRef: q.GetQuoteRef(),
|
|
IntentRef: strings.TrimSpace(q.GetIntentRef()),
|
|
Amounts: amounts,
|
|
Fees: fees,
|
|
FxQuote: toFxQuote(q.GetFxQuote()),
|
|
}
|
|
}
|
|
|
|
func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes {
|
|
if resp == nil {
|
|
return nil
|
|
}
|
|
items := make([]PaymentQuote, 0, len(resp.GetQuotes()))
|
|
for _, quote := range resp.GetQuotes() {
|
|
if dto := toPaymentQuote(quote); dto != nil {
|
|
items = append(items, *dto)
|
|
}
|
|
}
|
|
if len(items) == 0 {
|
|
items = nil
|
|
}
|
|
return &PaymentQuotes{
|
|
IdempotencyKey: resp.GetIdempotencyKey(),
|
|
QuoteRef: resp.GetQuoteRef(),
|
|
Items: items,
|
|
}
|
|
}
|
|
|
|
func toQuoteAmounts(q *quotationv2.PaymentQuote) *QuoteAmounts {
|
|
if q == nil {
|
|
return nil
|
|
}
|
|
amounts := &QuoteAmounts{
|
|
SourcePrincipal: toMoney(q.GetTransferPrincipalAmount()),
|
|
SourceDebitTotal: toMoney(q.GetPayerTotalDebitAmount()),
|
|
DestinationSettlement: toMoney(q.GetDestinationAmount()),
|
|
}
|
|
if amounts.SourcePrincipal == nil && amounts.SourceDebitTotal == nil && amounts.DestinationSettlement == nil {
|
|
return nil
|
|
}
|
|
return amounts
|
|
}
|
|
|
|
func toQuoteFees(lines []*feesv1.DerivedPostingLine) *QuoteFees {
|
|
feeLines := toFeeLines(lines)
|
|
if len(feeLines) == 0 {
|
|
return nil
|
|
}
|
|
return &QuoteFees{Lines: feeLines}
|
|
}
|
|
|
|
func toPayments(items []*orchestrationv2.Payment) []Payment {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
result := make([]Payment, 0, len(items))
|
|
for _, item := range items {
|
|
if p := toPayment(item); p != nil {
|
|
result = append(result, *p)
|
|
}
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
func toPayment(p *orchestrationv2.Payment) *Payment {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
operations := toUserVisibleOperations(p.GetStepExecutions(), p.GetQuoteSnapshot())
|
|
failureCode, failureReason := firstFailure(operations)
|
|
return &Payment{
|
|
PaymentRef: p.GetPaymentRef(),
|
|
State: enumJSONName(p.GetState().String()),
|
|
FailureCode: failureCode,
|
|
FailureReason: failureReason,
|
|
Operations: operations,
|
|
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
|
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
|
Meta: paymentMeta(p),
|
|
IdempotencyKey: "",
|
|
}
|
|
}
|
|
|
|
func firstFailure(operations []PaymentOperation) (string, string) {
|
|
for _, op := range operations {
|
|
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
|
|
continue
|
|
}
|
|
return strings.TrimSpace(op.FailureCode), strings.TrimSpace(op.FailureReason)
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func toUserVisibleOperations(steps []*orchestrationv2.StepExecution, quote *quotationv2.PaymentQuote) []PaymentOperation {
|
|
if len(steps) == 0 {
|
|
return nil
|
|
}
|
|
ops := make([]PaymentOperation, 0, len(steps))
|
|
for _, step := range steps {
|
|
if step == nil || !isUserVisibleStep(step.GetReportVisibility()) {
|
|
continue
|
|
}
|
|
ops = append(ops, toPaymentOperation(step, quote))
|
|
}
|
|
if len(ops) == 0 {
|
|
return nil
|
|
}
|
|
return ops
|
|
}
|
|
|
|
func toPaymentOperation(step *orchestrationv2.StepExecution, quote *quotationv2.PaymentQuote) PaymentOperation {
|
|
operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs())
|
|
amount, convertedAmount := operationAmounts(step.GetStepCode(), quote)
|
|
op := PaymentOperation{
|
|
StepRef: step.GetStepRef(),
|
|
Code: step.GetStepCode(),
|
|
State: enumJSONName(step.GetState().String()),
|
|
Label: strings.TrimSpace(step.GetUserLabel()),
|
|
Amount: amount,
|
|
ConvertedAmount: convertedAmount,
|
|
OperationRef: operationRef,
|
|
Gateway: string(gateway),
|
|
StartedAt: timestampAsTime(step.GetStartedAt()),
|
|
CompletedAt: timestampAsTime(step.GetCompletedAt()),
|
|
}
|
|
failure := step.GetFailure()
|
|
if failure == nil {
|
|
return op
|
|
}
|
|
op.FailureCode = enumJSONName(failure.GetCategory().String())
|
|
op.FailureReason = strings.TrimSpace(failure.GetMessage())
|
|
if op.FailureReason == "" {
|
|
op.FailureReason = strings.TrimSpace(failure.GetCode())
|
|
}
|
|
return op
|
|
}
|
|
|
|
func operationAmounts(stepCode string, quote *quotationv2.PaymentQuote) (*paymenttypes.Money, *paymenttypes.Money) {
|
|
if quote == nil {
|
|
return nil, nil
|
|
}
|
|
operation := stepOperationToken(stepCode)
|
|
|
|
primary := firstValidMoney(
|
|
toMoney(quote.GetDestinationAmount()),
|
|
toMoney(quote.GetTransferPrincipalAmount()),
|
|
toMoney(quote.GetPayerTotalDebitAmount()),
|
|
)
|
|
if operation != "fx_convert" {
|
|
return primary, nil
|
|
}
|
|
|
|
base := firstValidMoney(
|
|
toMoney(quote.GetTransferPrincipalAmount()),
|
|
toMoney(quote.GetPayerTotalDebitAmount()),
|
|
toMoney(quote.GetFxQuote().GetBaseAmount()),
|
|
)
|
|
quoteAmount := firstValidMoney(
|
|
toMoney(quote.GetDestinationAmount()),
|
|
toMoney(quote.GetFxQuote().GetQuoteAmount()),
|
|
)
|
|
return base, quoteAmount
|
|
}
|
|
|
|
func stepOperationToken(stepCode string) string {
|
|
parts := strings.Split(strings.ToLower(strings.TrimSpace(stepCode)), ".")
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(parts[len(parts)-1])
|
|
}
|
|
|
|
func firstValidMoney(values ...*paymenttypes.Money) *paymenttypes.Money {
|
|
for _, value := range values {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(value.GetAmount()) == "" || strings.TrimSpace(value.GetCurrency()) == "" {
|
|
continue
|
|
}
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
externalRefKindOperation = "operation_ref"
|
|
)
|
|
|
|
func operationRefAndGateway(stepCode string, refs []*orchestrationv2.ExternalReference) (string, mservice.Type) {
|
|
var (
|
|
operationRef string
|
|
gateway mservice.Type
|
|
)
|
|
|
|
for _, ref := range refs {
|
|
if ref == nil {
|
|
continue
|
|
}
|
|
|
|
kind := strings.ToLower(strings.TrimSpace(ref.GetKind()))
|
|
value := strings.TrimSpace(ref.GetRef())
|
|
candidateGateway := inferGatewayType(ref.GetGatewayInstanceId(), ref.GetRail(), stepCode)
|
|
|
|
if kind == externalRefKindOperation && operationRef == "" && value != "" {
|
|
operationRef = value
|
|
}
|
|
if gateway == "" && candidateGateway != "" {
|
|
gateway = candidateGateway
|
|
}
|
|
}
|
|
if gateway == "" {
|
|
gateway = inferGatewayType("", gatewayv1.Rail_RAIL_UNSPECIFIED, stepCode)
|
|
}
|
|
return operationRef, gateway
|
|
}
|
|
|
|
func inferGatewayType(gatewayInstanceID string, rail gatewayv1.Rail, stepCode string) mservice.Type {
|
|
if gateway := gatewayTypeFromInstanceID(gatewayInstanceID); gateway != "" {
|
|
return gateway
|
|
}
|
|
if gateway := gatewayTypeFromRail(rail); gateway != "" {
|
|
return gateway
|
|
}
|
|
return gatewayTypeFromStepCode(stepCode)
|
|
}
|
|
|
|
func gatewayTypeFromInstanceID(raw string) mservice.Type {
|
|
value := strings.ToLower(strings.TrimSpace(raw))
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
|
|
switch mservice.Type(value) {
|
|
case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger:
|
|
return mservice.Type(value)
|
|
}
|
|
|
|
switch {
|
|
case strings.Contains(value, "ledger"):
|
|
return mservice.Ledger
|
|
case strings.Contains(value, "tgsettle"):
|
|
return mservice.TgSettle
|
|
case strings.Contains(value, "payment_gateway"),
|
|
strings.Contains(value, "settlement"),
|
|
strings.Contains(value, "onramp"),
|
|
strings.Contains(value, "offramp"):
|
|
return mservice.PaymentGateway
|
|
case strings.Contains(value, "mntx"), strings.Contains(value, "mcards"):
|
|
return mservice.MntxGateway
|
|
case strings.Contains(value, "tron"):
|
|
return mservice.TronGateway
|
|
case strings.Contains(value, "chain"), strings.Contains(value, "crypto"):
|
|
return mservice.ChainGateway
|
|
case strings.Contains(value, "card"):
|
|
return mservice.MntxGateway
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func gatewayTypeFromRail(rail gatewayv1.Rail) mservice.Type {
|
|
switch rail {
|
|
case gatewayv1.Rail_RAIL_LEDGER:
|
|
return mservice.Ledger
|
|
case gatewayv1.Rail_RAIL_CARD:
|
|
return mservice.MntxGateway
|
|
case gatewayv1.Rail_RAIL_SETTLEMENT, gatewayv1.Rail_RAIL_ONRAMP, gatewayv1.Rail_RAIL_OFFRAMP:
|
|
return mservice.PaymentGateway
|
|
case gatewayv1.Rail_RAIL_CRYPTO:
|
|
return mservice.ChainGateway
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func gatewayTypeFromStepCode(stepCode string) mservice.Type {
|
|
code := strings.ToLower(strings.TrimSpace(stepCode))
|
|
switch {
|
|
case strings.Contains(code, "ledger"):
|
|
return mservice.Ledger
|
|
case strings.Contains(code, "card_payout"), strings.Contains(code, ".card."):
|
|
return mservice.MntxGateway
|
|
case strings.Contains(code, "provider_settlement"),
|
|
strings.Contains(code, "settlement"),
|
|
strings.Contains(code, "fx_convert"),
|
|
strings.Contains(code, "onramp"),
|
|
strings.Contains(code, "offramp"):
|
|
return mservice.PaymentGateway
|
|
case strings.Contains(code, "crypto"), strings.Contains(code, "chain"):
|
|
return mservice.ChainGateway
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool {
|
|
switch visibility {
|
|
case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,
|
|
orchestrationv2.ReportVisibility_REPORT_VISIBILITY_BACKOFFICE,
|
|
orchestrationv2.ReportVisibility_REPORT_VISIBILITY_AUDIT:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func paymentMeta(p *orchestrationv2.Payment) map[string]string {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
meta := make(map[string]string)
|
|
if quotationRef := strings.TrimSpace(p.GetQuotationRef()); quotationRef != "" {
|
|
meta["quotationRef"] = quotationRef
|
|
}
|
|
if clientPaymentRef := strings.TrimSpace(p.GetClientPaymentRef()); clientPaymentRef != "" {
|
|
meta["clientPaymentRef"] = clientPaymentRef
|
|
}
|
|
if version := p.GetVersion(); version > 0 {
|
|
meta["version"] = strconv.FormatUint(version, 10)
|
|
}
|
|
if len(meta) == 0 {
|
|
return nil
|
|
}
|
|
return meta
|
|
}
|
|
|
|
func timestampAsTime(ts *timestamppb.Timestamp) time.Time {
|
|
if ts == nil {
|
|
return time.Time{}
|
|
}
|
|
return ts.AsTime()
|
|
}
|
|
|
|
func enumJSONName(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|