payment quotation v2 + payment orchestration v2 draft
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package srequest
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
@@ -23,8 +25,7 @@ type QuotePayment struct {
|
||||
}
|
||||
|
||||
func (r *QuotePayment) Validate() error {
|
||||
// base checks
|
||||
if err := r.PaymentBase.Validate(); err != nil {
|
||||
if err := validateQuoteIdempotency(r.PreviewOnly, r.IdempotencyKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ type QuotePayments struct {
|
||||
}
|
||||
|
||||
func (r *QuotePayments) Validate() error {
|
||||
if err := r.PaymentBase.Validate(); err != nil {
|
||||
if err := validateQuoteIdempotency(r.PreviewOnly, r.IdempotencyKey); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(r.Intents) == 0 {
|
||||
@@ -57,6 +58,20 @@ func (r *QuotePayments) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateQuoteIdempotency(previewOnly bool, idempotencyKey string) error {
|
||||
key := strings.TrimSpace(idempotencyKey)
|
||||
if previewOnly {
|
||||
if key != "" {
|
||||
return merrors.InvalidArgument("previewOnly requests must not include idempotencyKey", "idempotencyKey")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if key == "" {
|
||||
return merrors.InvalidArgument("idempotencyKey is required", "idempotencyKey")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InitiatePayment struct {
|
||||
PaymentBase `json:",inline"`
|
||||
Intent *PaymentIntent `json:"intent,omitempty"`
|
||||
|
||||
29
api/server/interface/api/srequest/payment_validate_test.go
Normal file
29
api/server/interface/api/srequest/payment_validate_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package srequest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateQuoteIdempotency(t *testing.T) {
|
||||
t.Run("non-preview requires idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(false, ""); err == nil {
|
||||
t.Fatalf("expected error for empty idempotency key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preview rejects idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(true, "idem-1"); err == nil {
|
||||
t.Fatalf("expected error when preview request has idempotency key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preview accepts empty idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(true, ""); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-preview accepts idempotency key", func(t *testing.T) {
|
||||
if err := validateQuoteIdempotency(false, "idem-1"); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package sresponse
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,9 +12,9 @@ import (
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/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 {
|
||||
@@ -96,7 +97,7 @@ type paymentResponse struct {
|
||||
}
|
||||
|
||||
// PaymentQuote wraps a payment quote with refreshed access token.
|
||||
func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *sharedv1.PaymentQuote, token *TokenData) http.HandlerFunc {
|
||||
func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *quotationv2.PaymentQuote, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentQuoteResponse{
|
||||
Quote: toPaymentQuote(quote),
|
||||
IdempotencyKey: idempotencyKey,
|
||||
@@ -105,7 +106,7 @@ func PaymentQuoteResponse(logger mlogger.Logger, idempotencyKey string, quote *s
|
||||
}
|
||||
|
||||
// PaymentQuotes wraps batch quotes with refreshed access token.
|
||||
func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv1.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv2.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentQuotesResponse{
|
||||
Quote: toPaymentQuotes(resp),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
@@ -113,7 +114,7 @@ func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv1.QuotePayment
|
||||
}
|
||||
|
||||
// Payments wraps a list of payments with refreshed access token.
|
||||
func PaymentsResponse(logger mlogger.Logger, payments []*sharedv1.Payment, token *TokenData) http.HandlerFunc {
|
||||
func PaymentsResponse(logger mlogger.Logger, payments []*orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentsResponse{
|
||||
Payments: toPayments(payments),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
@@ -121,7 +122,7 @@ func PaymentsResponse(logger mlogger.Logger, payments []*sharedv1.Payment, token
|
||||
}
|
||||
|
||||
// PaymentsList wraps a list of payments with refreshed access token and pagination data.
|
||||
func PaymentsListResponse(logger mlogger.Logger, resp *orchestratorv1.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
func PaymentsListResponse(logger mlogger.Logger, resp *orchestrationv2.ListPaymentsResponse, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentsResponse{
|
||||
Payments: toPayments(resp.GetPayments()),
|
||||
Page: resp.GetPage(),
|
||||
@@ -130,7 +131,7 @@ func PaymentsListResponse(logger mlogger.Logger, resp *orchestratorv1.ListPaymen
|
||||
}
|
||||
|
||||
// Payment wraps a payment with refreshed access token.
|
||||
func PaymentResponse(logger mlogger.Logger, payment *sharedv1.Payment, token *TokenData) http.HandlerFunc {
|
||||
func PaymentResponse(logger mlogger.Logger, payment *orchestrationv2.Payment, token *TokenData) http.HandlerFunc {
|
||||
return response.Ok(logger, paymentResponse{
|
||||
Payment: toPayment(payment),
|
||||
authResponse: authResponse{AccessToken: *token},
|
||||
@@ -191,33 +192,20 @@ func toFxQuote(q *oraclev1.Quote) *FxQuote {
|
||||
}
|
||||
}
|
||||
|
||||
func toPaymentQuote(q *sharedv1.PaymentQuote) *PaymentQuote {
|
||||
func toPaymentQuote(q *quotationv2.PaymentQuote) *PaymentQuote {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
return &PaymentQuote{
|
||||
QuoteRef: q.GetQuoteRef(),
|
||||
DebitAmount: toMoney(q.GetDebitAmount()),
|
||||
DebitSettlementAmount: toMoney(q.GetDebitSettlementAmount()),
|
||||
ExpectedSettlementAmount: toMoney(q.GetExpectedSettlementAmount()),
|
||||
ExpectedFeeTotal: toMoney(q.GetExpectedFeeTotal()),
|
||||
DebitAmount: toMoney(q.GetPayerTotalDebitAmount()),
|
||||
ExpectedSettlementAmount: toMoney(q.GetDestinationAmount()),
|
||||
FeeLines: toFeeLines(q.GetFeeLines()),
|
||||
FxQuote: toFxQuote(q.GetFxQuote()),
|
||||
}
|
||||
}
|
||||
|
||||
func toPaymentQuoteAggregate(q *sharedv1.PaymentQuoteAggregate) *PaymentQuoteAggregate {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
return &PaymentQuoteAggregate{
|
||||
DebitAmounts: toMoneyList(q.GetDebitAmounts()),
|
||||
ExpectedSettlementAmounts: toMoneyList(q.GetExpectedSettlementAmounts()),
|
||||
ExpectedFeeTotals: toMoneyList(q.GetExpectedFeeTotals()),
|
||||
}
|
||||
}
|
||||
|
||||
func toPaymentQuotes(resp *quotationv1.QuotePaymentsResponse) *PaymentQuotes {
|
||||
func toPaymentQuotes(resp *quotationv2.QuotePaymentsResponse) *PaymentQuotes {
|
||||
if resp == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -233,12 +221,11 @@ func toPaymentQuotes(resp *quotationv1.QuotePaymentsResponse) *PaymentQuotes {
|
||||
return &PaymentQuotes{
|
||||
IdempotencyKey: resp.GetIdempotencyKey(),
|
||||
QuoteRef: resp.GetQuoteRef(),
|
||||
Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()),
|
||||
Quotes: quotes,
|
||||
}
|
||||
}
|
||||
|
||||
func toPayments(items []*sharedv1.Payment) []Payment {
|
||||
func toPayments(items []*orchestrationv2.Payment) []Payment {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -254,22 +241,65 @@ func toPayments(items []*sharedv1.Payment) []Payment {
|
||||
return result
|
||||
}
|
||||
|
||||
func toPayment(p *sharedv1.Payment) *Payment {
|
||||
func toPayment(p *orchestrationv2.Payment) *Payment {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
failureCode, failureReason := firstFailure(p.GetStepExecutions())
|
||||
return &Payment{
|
||||
PaymentRef: p.GetPaymentRef(),
|
||||
IdempotencyKey: p.GetIdempotencyKey(),
|
||||
State: enumJSONName(p.GetState().String()),
|
||||
FailureCode: enumJSONName(p.GetFailureCode().String()),
|
||||
FailureReason: p.GetFailureReason(),
|
||||
LastQuote: toPaymentQuote(p.GetLastQuote()),
|
||||
CreatedAt: p.GetCreatedAt().AsTime(),
|
||||
Meta: p.GetMetadata(),
|
||||
FailureCode: failureCode,
|
||||
FailureReason: failureReason,
|
||||
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
|
||||
CreatedAt: timestampAsTime(p.GetCreatedAt()),
|
||||
Meta: paymentMeta(p),
|
||||
IdempotencyKey: "",
|
||||
}
|
||||
}
|
||||
|
||||
func firstFailure(steps []*orchestrationv2.StepExecution) (string, string) {
|
||||
for _, step := range steps {
|
||||
if step == nil || step.GetFailure() == nil {
|
||||
continue
|
||||
}
|
||||
failure := step.GetFailure()
|
||||
message := strings.TrimSpace(failure.GetMessage())
|
||||
if message == "" {
|
||||
message = strings.TrimSpace(failure.GetCode())
|
||||
}
|
||||
return enumJSONName(failure.GetCategory().String()), message
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user