From f7b09153031b570d7a4d1b08144f93d189eb6c50 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 4 Mar 2026 18:02:36 +0100 Subject: [PATCH] op payment info added --- .../bff/interface/api/sresponse/payment.go | 97 ++++++++++++++----- .../interface/api/sresponse/payment_test.go | 63 +++++++++++- .../internal/server/paymentapiimp/service.go | 2 +- interface/models/payment/payment.yaml | 8 +- 4 files changed, 142 insertions(+), 28 deletions(-) diff --git a/api/edge/bff/interface/api/sresponse/payment.go b/api/edge/bff/interface/api/sresponse/payment.go index 30ea42de..f21dc99c 100644 --- a/api/edge/bff/interface/api/sresponse/payment.go +++ b/api/edge/bff/interface/api/sresponse/payment.go @@ -79,16 +79,18 @@ type Payment struct { } type PaymentOperation struct { - StepRef string `json:"stepRef,omitempty"` - Code string `json:"code,omitempty"` - State string `json:"state,omitempty"` - Label string `json:"label,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"` + 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 { @@ -287,7 +289,7 @@ func toPayment(p *orchestrationv2.Payment) *Payment { if p == nil { return nil } - operations := toUserVisibleOperations(p.GetStepExecutions()) + operations := toUserVisibleOperations(p.GetStepExecutions(), p.GetQuoteSnapshot()) failureCode, failureReason := firstFailure(operations) return &Payment{ PaymentRef: p.GetPaymentRef(), @@ -312,7 +314,7 @@ func firstFailure(operations []PaymentOperation) (string, string) { return "", "" } -func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOperation { +func toUserVisibleOperations(steps []*orchestrationv2.StepExecution, quote *quotationv2.PaymentQuote) []PaymentOperation { if len(steps) == 0 { return nil } @@ -321,7 +323,7 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp if step == nil || !isUserVisibleStep(step.GetReportVisibility()) { continue } - ops = append(ops, toPaymentOperation(step)) + ops = append(ops, toPaymentOperation(step, quote)) } if len(ops) == 0 { return nil @@ -329,17 +331,20 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp return ops } -func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { +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()), - OperationRef: operationRef, - Gateway: string(gateway), - StartedAt: timestampAsTime(step.GetStartedAt()), - CompletedAt: timestampAsTime(step.GetCompletedAt()), + 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 { @@ -353,6 +358,54 @@ func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { 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" ) diff --git a/api/edge/bff/interface/api/sresponse/payment_test.go b/api/edge/bff/interface/api/sresponse/payment_test.go index 6143f46a..90cf6e32 100644 --- a/api/edge/bff/interface/api/sresponse/payment_test.go +++ b/api/edge/bff/interface/api/sresponse/payment_test.go @@ -4,6 +4,7 @@ import ( "testing" gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/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" @@ -33,7 +34,7 @@ func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) { }, } - ops := toUserVisibleOperations(steps) + ops := toUserVisibleOperations(steps, nil) if len(ops) != 2 { t.Fatalf("operations count mismatch: got=%d want=2", len(ops)) } @@ -149,7 +150,7 @@ func TestToPaymentOperation_MapsOperationRefAndGateway(t *testing.T) { Ref: "op-123", }, }, - }) + }, nil) if got, want := op.OperationRef, "op-123"; got != want { t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want) @@ -164,7 +165,7 @@ func TestToPaymentOperation_InfersGatewayFromStepCode(t *testing.T) { StepRef: "step-2", StepCode: "edge.1_2.ledger.debit", State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, - }) + }, nil) if got := op.OperationRef; got != "" { t.Fatalf("expected empty operation_ref, got=%q", got) @@ -187,7 +188,7 @@ func TestToPaymentOperation_DoesNotFallbackToCardPayoutRef(t *testing.T) { Ref: "payout-123", }, }, - }) + }, nil) if got := op.OperationRef; got != "" { t.Fatalf("expected empty operation_ref, got=%q", got) @@ -196,3 +197,57 @@ func TestToPaymentOperation_DoesNotFallbackToCardPayoutRef(t *testing.T) { t.Fatalf("gateway mismatch: got=%q want=%q", got, want) } } + +func TestToPaymentOperation_MapsAmount(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-4", + StepCode: "hop.4.card_payout.send", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + }, "ationv2.PaymentQuote{ + TransferPrincipalAmount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"}, + DestinationAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + }) + + if op.Amount == nil { + t.Fatal("expected amount to be mapped") + } + if got, want := op.Amount.Amount, "100.00"; got != want { + t.Fatalf("amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Amount.Currency, "EUR"; got != want { + t.Fatalf("amount.currency mismatch: got=%q want=%q", got, want) + } + if got := op.ConvertedAmount; got != nil { + t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got) + } +} + +func TestToPaymentOperation_MapsFxTwoAmounts(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-5", + StepCode: "hop.2.settlement.fx_convert", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + }, "ationv2.PaymentQuote{ + TransferPrincipalAmount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"}, + DestinationAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + }) + + if op.Amount == nil { + t.Fatal("expected fx base amount to be mapped") + } + if got, want := op.Amount.Amount, "110.00"; got != want { + t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Amount.Currency, "USDT"; got != want { + t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want) + } + if op.ConvertedAmount == nil { + t.Fatal("expected fx converted amount to be mapped") + } + if got, want := op.ConvertedAmount.Amount, "100.00"; got != want { + t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.ConvertedAmount.Currency, "EUR"; got != want { + t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want) + } +} diff --git a/api/edge/bff/internal/server/paymentapiimp/service.go b/api/edge/bff/internal/server/paymentapiimp/service.go index 952d4683..3424a98d 100644 --- a/api/edge/bff/internal/server/paymentapiimp/service.go +++ b/api/edge/bff/internal/server/paymentapiimp/service.go @@ -207,7 +207,7 @@ type grpcQuotationClient struct { callTimeout time.Duration } -func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) { +func newQuotationClient(_ context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) { cfg.setDefaults() if strings.TrimSpace(cfg.Address) == "" { return nil, merrors.InvalidArgument("payment quotation: address is required") diff --git a/interface/models/payment/payment.yaml b/interface/models/payment/payment.yaml index 64a2c0e9..cd5604b1 100644 --- a/interface/models/payment/payment.yaml +++ b/interface/models/payment/payment.yaml @@ -397,8 +397,14 @@ components: label: description: Human-readable operation label. type: string + amount: + description: Primary money amount associated with the operation. + $ref: ../common/money.yaml#/components/schemas/Money + convertedAmount: + description: Secondary amount for conversion operations (for example FX convert output amount). + $ref: ../common/money.yaml#/components/schemas/Money operationRef: - description: Internal operation reference identifier reported by the gateway. + description: External operation reference identifier reported by the gateway. type: string gateway: description: Gateway microservice type handling the operation.