From 3fcbbfb08a379c1555a18dbc282c043cea765083 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 4 Mar 2026 13:51:48 +0100 Subject: [PATCH] added gateway and operation references --- .../bff/interface/api/sresponse/payment.go | 130 +++++++++++++++++- .../interface/api/sresponse/payment_test.go | 62 +++++++++ interface/models/payment/payment.yaml | 6 + 3 files changed, 192 insertions(+), 6 deletions(-) diff --git a/api/edge/bff/interface/api/sresponse/payment.go b/api/edge/bff/interface/api/sresponse/payment.go index e8ca3e83..30ea42de 100644 --- a/api/edge/bff/interface/api/sresponse/payment.go +++ b/api/edge/bff/interface/api/sresponse/payment.go @@ -8,8 +8,10 @@ import ( "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" @@ -81,6 +83,8 @@ type PaymentOperation struct { 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"` @@ -326,13 +330,16 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp } func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { + operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs()) op := PaymentOperation{ - StepRef: step.GetStepRef(), - Code: step.GetStepCode(), - State: enumJSONName(step.GetState().String()), - Label: strings.TrimSpace(step.GetUserLabel()), - StartedAt: timestampAsTime(step.GetStartedAt()), - CompletedAt: timestampAsTime(step.GetCompletedAt()), + 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()), } failure := step.GetFailure() if failure == nil { @@ -346,6 +353,117 @@ func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { return op } +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, diff --git a/api/edge/bff/interface/api/sresponse/payment_test.go b/api/edge/bff/interface/api/sresponse/payment_test.go index a6de4f37..6143f46a 100644 --- a/api/edge/bff/interface/api/sresponse/payment_test.go +++ b/api/edge/bff/interface/api/sresponse/payment_test.go @@ -3,6 +3,7 @@ package sresponse import ( "testing" + gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/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" @@ -134,3 +135,64 @@ func TestToPaymentQuote_MapsIntentRef(t *testing.T) { t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want) } } + +func TestToPaymentOperation_MapsOperationRefAndGateway(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-1", + StepCode: "hop.4.card_payout.send", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + Refs: []*orchestrationv2.ExternalReference{ + { + Rail: gatewayv1.Rail_RAIL_CARD, + GatewayInstanceId: "mcards", + Kind: "operation_ref", + Ref: "op-123", + }, + }, + }) + + if got, want := op.OperationRef, "op-123"; got != want { + t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want) + } + if got, want := op.Gateway, "mntx_gateway"; got != want { + t.Fatalf("gateway mismatch: got=%q want=%q", got, want) + } +} + +func TestToPaymentOperation_InfersGatewayFromStepCode(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-2", + StepCode: "edge.1_2.ledger.debit", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + }) + + if got := op.OperationRef; got != "" { + t.Fatalf("expected empty operation_ref, got=%q", got) + } + if got, want := op.Gateway, "ledger"; got != want { + t.Fatalf("gateway mismatch: got=%q want=%q", got, want) + } +} + +func TestToPaymentOperation_DoesNotFallbackToCardPayoutRef(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-3", + StepCode: "hop.4.card_payout.send", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + Refs: []*orchestrationv2.ExternalReference{ + { + Rail: gatewayv1.Rail_RAIL_CARD, + GatewayInstanceId: "mcards", + Kind: "card_payout_ref", + Ref: "payout-123", + }, + }, + }) + + if got := op.OperationRef; got != "" { + t.Fatalf("expected empty operation_ref, got=%q", got) + } + if got, want := op.Gateway, "mntx_gateway"; got != want { + t.Fatalf("gateway mismatch: got=%q want=%q", got, want) + } +} diff --git a/interface/models/payment/payment.yaml b/interface/models/payment/payment.yaml index 7685511c..60747533 100644 --- a/interface/models/payment/payment.yaml +++ b/interface/models/payment/payment.yaml @@ -397,6 +397,12 @@ components: label: description: Human-readable operation label. type: string + operationRef: + description: External operation reference identifier reported by the gateway. + type: string + gateway: + description: Gateway microservice type handling the operation. + type: string failureCode: description: Machine-readable failure code when operation fails. type: string