fixed po <-> tgsettle contract

This commit is contained in:
Stephan D
2026-03-06 15:12:14 +01:00
parent 031b8931ca
commit 3295b9d9f0
15 changed files with 540 additions and 76 deletions

View File

@@ -27,7 +27,7 @@ require (
github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
)

View File

@@ -213,8 +213,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -45,6 +45,9 @@ const (
type StepShell struct {
StepRef string `bson:"stepRef" json:"stepRef"`
StepCode string `bson:"stepCode" json:"stepCode"`
Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"`
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
}
@@ -53,6 +56,9 @@ type StepShell struct {
type StepExecution struct {
StepRef string `bson:"stepRef" json:"stepRef"`
StepCode string `bson:"stepCode" json:"stepCode"`
Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"`
Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"`
InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"`
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
State StepState `bson:"state" json:"state"`

View File

@@ -143,10 +143,16 @@ func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].report_visibility is invalid")
}
userLabel := strings.TrimSpace(shell[i].UserLabel)
railValue := model.ParseRail(string(shell[i].Rail))
gatewayID := strings.TrimSpace(shell[i].Gateway)
instanceID := strings.TrimSpace(shell[i].InstanceID)
out = append(out, StepExecution{
StepRef: stepRef,
StepCode: stepCode,
Rail: railValue,
Gateway: gatewayID,
InstanceID: instanceID,
ReportVisibility: visibility,
UserLabel: userLabel,
State: StepStatePending,

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/merrors"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
"go.mongodb.org/mongo-driver/v2/bson"
@@ -42,7 +43,15 @@ func TestCreate_OK(t *testing.T) {
QuoteSnapshot: quote,
Steps: []StepShell{
{StepRef: " s1 ", StepCode: " reserve_funds ", ReportVisibility: model.ReportVisibilityHidden},
{StepRef: "s2", StepCode: "submit_gateway", ReportVisibility: model.ReportVisibilityUser, UserLabel: " Card payout "},
{
StepRef: "s2",
StepCode: "submit_gateway",
Rail: discovery.RailProviderSettlement,
Gateway: "payment_gateway_settlement",
InstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
ReportVisibility: model.ReportVisibilityUser,
UserLabel: " Card payout ",
},
},
})
if err != nil {
@@ -111,6 +120,15 @@ func TestCreate_OK(t *testing.T) {
if got, want := payment.StepExecutions[1].UserLabel, "Card payout"; got != want {
t.Fatalf("unexpected second step user label: got=%q want=%q", got, want)
}
if got, want := payment.StepExecutions[1].Rail, model.Rail(discovery.RailProviderSettlement); got != want {
t.Fatalf("unexpected second step rail: got=%q want=%q", got, want)
}
if got, want := payment.StepExecutions[1].Gateway, "payment_gateway_settlement"; got != want {
t.Fatalf("unexpected second step gateway: got=%q want=%q", got, want)
}
if got, want := payment.StepExecutions[1].InstanceID, "04a54fec-20f4-4250-a715-eb9886e13e12"; got != want {
t.Fatalf("unexpected second step instance_id: got=%q want=%q", got, want)
}
// Verify immutable snapshot semantics by ensuring clones were created.
payment.IntentSnapshot.Ref = "changed"

View File

@@ -221,6 +221,9 @@ func toStepShells(graph *xplan.Graph) []agg.StepShell {
out = append(out, agg.StepShell{
StepRef: graph.Steps[i].StepRef,
StepCode: graph.Steps[i].StepCode,
Rail: graph.Steps[i].Rail,
Gateway: graph.Steps[i].Gateway,
InstanceID: graph.Steps[i].InstanceID,
ReportVisibility: graph.Steps[i].Visibility,
UserLabel: graph.Steps[i].UserLabel,
})

View File

@@ -408,6 +408,15 @@ func stepExecutionEqual(left, right agg.StepExecution) bool {
if left.StepRef != right.StepRef || left.StepCode != right.StepCode {
return false
}
if left.Rail != right.Rail {
return false
}
if strings.TrimSpace(left.Gateway) != strings.TrimSpace(right.Gateway) {
return false
}
if strings.TrimSpace(left.InstanceID) != strings.TrimSpace(right.InstanceID) {
return false
}
if left.State != right.State || left.Attempt != right.Attempt {
return false
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/merrors"
)
@@ -144,6 +145,16 @@ func (s *svc) normalizeStepExecutions(
stepCode = stepsByRef[stepRef].StepCode
}
exec.StepCode = stepCode
step := stepsByRef[stepRef]
if exec.Rail == discovery.RailUnspecified {
exec.Rail = step.Rail
}
if strings.TrimSpace(exec.Gateway) == "" {
exec.Gateway = strings.TrimSpace(step.Gateway)
}
if strings.TrimSpace(exec.InstanceID) == "" {
exec.InstanceID = strings.TrimSpace(step.InstanceID)
}
exec.ReportVisibility = effectiveStepVisibility(exec.ReportVisibility, stepsByRef[stepRef].Visibility)
exec.UserLabel = firstNonEmpty(exec.UserLabel, stepsByRef[stepRef].UserLabel)
cloned := cloneStepExecution(exec)
@@ -158,6 +169,9 @@ func (s *svc) normalizeStepExecution(exec agg.StepExecution, index int) (agg.Ste
exec.FailureCode = strings.TrimSpace(exec.FailureCode)
exec.FailureMsg = strings.TrimSpace(exec.FailureMsg)
exec.UserLabel = strings.TrimSpace(exec.UserLabel)
exec.Gateway = strings.TrimSpace(exec.Gateway)
exec.InstanceID = strings.TrimSpace(exec.InstanceID)
exec.Rail = model.ParseRail(string(exec.Rail))
exec.ReportVisibility = model.NormalizeReportVisibility(exec.ReportVisibility)
exec.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
if exec.StepRef == "" {
@@ -197,6 +211,9 @@ func seedMissingExecutions(
executionsByRef[stepRef] = &agg.StepExecution{
StepRef: step.StepRef,
StepCode: step.StepCode,
Rail: step.Rail,
Gateway: strings.TrimSpace(step.Gateway),
InstanceID: strings.TrimSpace(step.InstanceID),
ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
UserLabel: strings.TrimSpace(step.UserLabel),
State: agg.StepStatePending,

View File

@@ -13,7 +13,6 @@ import (
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
"github.com/tech/sendico/payments/storage/model"
cons "github.com/tech/sendico/pkg/messaging/consumer"
paymentgatewaynotifications "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
@@ -412,6 +411,9 @@ func buildObserveCandidate(step agg.StepExecution) (runningObserveCandidate, boo
}
}
}
if candidate.gatewayInstanceID == "" {
candidate.gatewayInstanceID = strings.TrimSpace(step.InstanceID)
}
if candidate.stepRef == "" || candidate.transferRef == "" {
return runningObserveCandidate{}, false
}
@@ -475,7 +477,7 @@ func (s *Service) pollObserveCandidate(ctx context.Context, payment *agg.Payment
StepRef: candidate.stepRef,
OperationRef: firstNonEmpty(strings.TrimSpace(transfer.GetOperationRef()), candidate.operationRef),
TransferRef: strings.TrimSpace(candidate.transferRef),
GatewayInstanceID: firstNonEmpty(candidate.gatewayInstanceID, strings.TrimSpace(gateway.InstanceID), strings.TrimSpace(gateway.ID)),
GatewayInstanceID: resolvedObserveGatewayID(candidate.gatewayInstanceID, gateway),
Status: status,
}
switch status {
@@ -517,39 +519,106 @@ func (s *Service) pollObserveCandidate(ctx context.Context, payment *agg.Payment
}
func (s *Service) resolveObserveGateway(ctx context.Context, payment *agg.Payment, candidate runningObserveCandidate) (*model.GatewayInstanceDescriptor, error) {
if s == nil || s.gatewayRegistry == nil {
return nil, errors.New("observe polling: gateway registry is unavailable")
}
items, err := s.gatewayRegistry.List(ctx)
if err != nil {
return nil, err
}
hint, hasHint := observeStepGatewayHint(payment, candidate.stepRef)
expectedRail := model.Rail(discovery.RailUnspecified)
if hasHint {
expectedRail = hint.rail
}
if gatewayID := strings.TrimSpace(candidate.gatewayInstanceID); gatewayID != "" {
items, err := s.gatewayRegistry.List(ctx)
if err == nil {
for i := range items {
item := items[i]
if item == nil || !item.IsEnabled {
continue
}
if !strings.EqualFold(strings.TrimSpace(item.ID), gatewayID) && !strings.EqualFold(strings.TrimSpace(item.InstanceID), gatewayID) {
continue
}
if strings.TrimSpace(item.InvokeURI) == "" {
continue
}
return item, nil
}
if item := findEnabledGatewayDescriptor(items, gatewayID, expectedRail); item != nil {
return item, nil
}
}
if hasHint {
if item := findEnabledGatewayDescriptor(items, hint.instanceID, hint.rail); item != nil {
return item, nil
}
if item := findEnabledGatewayDescriptor(items, hint.gatewayID, hint.rail); item != nil {
return item, nil
}
}
return nil, errors.New("observe polling: gateway instance not found")
}
executor := gatewayCryptoExecutor{
gatewayRegistry: s.gatewayRegistry,
type observeStepHint struct {
rail model.Rail
gatewayID string
instanceID string
}
func observeStepGatewayHint(payment *agg.Payment, stepRef string) (observeStepHint, bool) {
if payment == nil {
return observeStepHint{}, false
}
step := xplan.Step{
Rail: discovery.RailCrypto,
key := strings.TrimSpace(stepRef)
if key == "" {
return observeStepHint{}, false
}
if gatewayID := strings.TrimSpace(candidate.gatewayInstanceID); gatewayID != "" {
step.InstanceID = gatewayID
step.Gateway = gatewayID
} else if gateway, instanceID, ok := sourceCryptoHop(payment); ok {
step.Gateway = strings.TrimSpace(gateway)
step.InstanceID = strings.TrimSpace(instanceID)
for i := range payment.StepExecutions {
step := payment.StepExecutions[i]
if !strings.EqualFold(strings.TrimSpace(step.StepRef), key) {
continue
}
hint := observeStepHint{
rail: model.ParseRail(string(step.Rail)),
gatewayID: strings.TrimSpace(step.Gateway),
instanceID: strings.TrimSpace(step.InstanceID),
}
if hint.gatewayID == "" && hint.instanceID == "" {
return observeStepHint{}, false
}
return hint, true
}
return executor.resolveGateway(ctx, step)
return observeStepHint{}, false
}
func findEnabledGatewayDescriptor(items []*model.GatewayInstanceDescriptor, identifier string, rail model.Rail) *model.GatewayInstanceDescriptor {
key := strings.TrimSpace(identifier)
if key == "" {
return nil
}
for i := range items {
item := items[i]
if item == nil || !item.IsEnabled || strings.TrimSpace(item.InvokeURI) == "" {
continue
}
if rail != model.Rail(discovery.RailUnspecified) && model.ParseRail(string(item.Rail)) != rail {
continue
}
if strings.EqualFold(strings.TrimSpace(item.ID), key) || strings.EqualFold(strings.TrimSpace(item.InstanceID), key) {
return item
}
}
return nil
}
func resolvedObserveGatewayID(candidateGatewayID string, gateway *model.GatewayInstanceDescriptor) string {
candidateID := strings.TrimSpace(candidateGatewayID)
if candidateID != "" && gatewayIdentifierMatches(gateway, candidateID) {
return candidateID
}
if gateway == nil {
return ""
}
return firstNonEmpty(strings.TrimSpace(gateway.InstanceID), strings.TrimSpace(gateway.ID))
}
func gatewayIdentifierMatches(gateway *model.GatewayInstanceDescriptor, identifier string) bool {
if gateway == nil {
return false
}
key := strings.TrimSpace(identifier)
if key == "" {
return false
}
return strings.EqualFold(strings.TrimSpace(gateway.ID), key) || strings.EqualFold(strings.TrimSpace(gateway.InstanceID), key)
}
func mapTransferStatus(status chainv1.TransferStatus) (gatewayStatus erecon.GatewayStatus, terminal bool, ok bool) {

View File

@@ -3,14 +3,15 @@ package orchestrator
import (
"context"
"errors"
"github.com/tech/sendico/pkg/discovery"
"testing"
chainclient "github.com/tech/sendico/gateway/chain/client"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/discovery"
pm "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/payments/rail"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
@@ -412,6 +413,30 @@ func TestRunningObserveCandidates_UsesCardPayoutRefAsTransfer(t *testing.T) {
}
}
func TestRunningObserveCandidates_UsesPlannedStepInstanceWhenExternalRefGatewayMissing(t *testing.T) {
payment := &agg.Payment{
StepExecutions: []agg.StepExecution{
{
StepRef: "hop_2_settlement_observe",
StepCode: "hop.2.settlement.observe",
InstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
State: agg.StepStateRunning,
ExternalRefs: []agg.ExternalRef{
{Kind: erecon.ExternalRefKindTransfer, Ref: "trf-2"},
},
},
},
}
candidates := runningObserveCandidates(payment)
if len(candidates) != 1 {
t.Fatalf("candidate count mismatch: got=%d want=1", len(candidates))
}
if got, want := candidates[0].gatewayInstanceID, "04a54fec-20f4-4250-a715-eb9886e13e12"; got != want {
t.Fatalf("gateway_instance_id mismatch: got=%q want=%q", got, want)
}
}
func TestResolveObserveGateway_UsesExternalRefGatewayInstanceAcrossRails(t *testing.T) {
svc := &Service{
gatewayRegistry: &fakeGatewayRegistry{
@@ -466,5 +491,192 @@ func TestResolveObserveGateway_UsesExternalRefGatewayInstanceAcrossRails(t *test
}
}
func TestResolveObserveGateway_UsesPlannedStepGatewayWhenExternalRefInstanceIsStale(t *testing.T) {
svc := &Service{
gatewayRegistry: &fakeGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "payment_gateway_settlement",
InstanceID: "ea2600ce-3de6-4cc5-bd1e-e26ebaceb6b4",
Rail: discovery.RailProviderSettlement,
InvokeURI: "grpc://tgsettle-gateway-new",
IsEnabled: true,
},
{
ID: "crypto_rail_gateway_tron_mainnet",
InstanceID: "fbef2c3b-ff66-447e-8bba-fa666a955855",
Rail: discovery.RailCrypto,
InvokeURI: "grpc://tron-gateway",
IsEnabled: true,
},
},
},
}
payment := &agg.Payment{
StepExecutions: []agg.StepExecution{
{
StepRef: "hop_2_settlement_observe",
StepCode: "hop.2.settlement.observe",
Rail: discovery.RailProviderSettlement,
Gateway: "payment_gateway_settlement",
InstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
},
},
}
gateway, err := svc.resolveObserveGateway(context.Background(), payment, runningObserveCandidate{
stepRef: "hop_2_settlement_observe",
transferRef: "trf-1",
gatewayInstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
})
if err != nil {
t.Fatalf("resolveObserveGateway returned error: %v", err)
}
if gateway == nil {
t.Fatal("expected gateway")
}
if got, want := gateway.ID, "payment_gateway_settlement"; got != want {
t.Fatalf("gateway id mismatch: got=%q want=%q", got, want)
}
if got, want := gateway.InstanceID, "ea2600ce-3de6-4cc5-bd1e-e26ebaceb6b4"; got != want {
t.Fatalf("gateway instance mismatch: got=%q want=%q", got, want)
}
}
func TestResolveObserveGateway_FailsWhenPlannedGatewayMetadataIsMissing(t *testing.T) {
svc := &Service{
gatewayRegistry: &fakeGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "crypto_rail_gateway_tron_mainnet",
InstanceID: "fbef2c3b-ff66-447e-8bba-fa666a955855",
Rail: discovery.RailCrypto,
InvokeURI: "grpc://tron-gateway",
IsEnabled: true,
},
},
},
}
payment := &agg.Payment{
QuoteSnapshot: &model.PaymentQuoteSnapshot{
Route: &paymenttypes.QuoteRouteSpecification{
Hops: []*paymenttypes.QuoteRouteHop{
{
Index: 1,
Rail: "CRYPTO",
Gateway: "crypto_rail_gateway_tron_mainnet",
InstanceID: "fbef2c3b-ff66-447e-8bba-fa666a955855",
Role: paymenttypes.QuoteRouteHopRoleSource,
},
},
},
},
}
gateway, err := svc.resolveObserveGateway(context.Background(), payment, runningObserveCandidate{
stepRef: "hop_2_settlement_observe",
transferRef: "trf-1",
gatewayInstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
})
if err == nil {
t.Fatal("expected gateway resolution error")
}
if gateway != nil {
t.Fatal("expected nil gateway on resolution failure")
}
}
func TestPollObserveCandidate_UsesResolvedGatewayAfterInstanceRotation(t *testing.T) {
orgID := bson.NewObjectID()
transferRef := "b6874b55-20b0-425d-9e47-d430964b1616:hop_2_settlement_fx_convert"
operationRef := "69aabf823555e083d23b2964:hop_2_settlement_fx_convert"
var requestedTransferRef string
client := &chainclient.Fake{
GetTransferFn: func(_ context.Context, req *chainv1.GetTransferRequest) (*chainv1.GetTransferResponse, error) {
requestedTransferRef = req.GetTransferRef()
return &chainv1.GetTransferResponse{
Transfer: &chainv1.Transfer{
TransferRef: req.GetTransferRef(),
OperationRef: operationRef,
Status: chainv1.TransferStatus_TRANSFER_SUCCESS,
},
}, nil
},
}
resolver := &fakeGatewayInvokeResolver{client: client}
v2 := &fakeExternalRuntimeV2{}
svc := &Service{
logger: zap.NewNop(),
v2: v2,
gatewayInvokeResolver: resolver,
gatewayRegistry: &fakeGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "payment_gateway_settlement",
InstanceID: "ea2600ce-3de6-4cc5-bd1e-e26ebaceb6b4",
Rail: discovery.RailProviderSettlement,
InvokeURI: "grpc://tgsettle-gateway-new",
IsEnabled: true,
},
},
},
}
payment := &agg.Payment{
OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID},
PaymentRef: "69aabf823555e083d23b2964",
StepExecutions: []agg.StepExecution{
{
StepRef: "hop_2_settlement_observe",
StepCode: "hop.2.settlement.observe",
Rail: discovery.RailProviderSettlement,
Gateway: "payment_gateway_settlement",
InstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
State: agg.StepStateRunning,
ExternalRefs: []agg.ExternalRef{
{
GatewayInstanceID: "04a54fec-20f4-4250-a715-eb9886e13e12",
Kind: erecon.ExternalRefKindTransfer,
Ref: transferRef,
},
},
},
},
}
candidates := runningObserveCandidates(payment)
if len(candidates) != 1 {
t.Fatalf("candidate count mismatch: got=%d want=1", len(candidates))
}
svc.pollObserveCandidate(context.Background(), payment, candidates[0])
if got, want := resolver.lastInvokeURI, "grpc://tgsettle-gateway-new"; got != want {
t.Fatalf("invoke uri mismatch: got=%q want=%q", got, want)
}
if got, want := requestedTransferRef, transferRef; got != want {
t.Fatalf("transfer_ref lookup mismatch: got=%q want=%q", got, want)
}
if v2.reconcileInput == nil || v2.reconcileInput.Event.Gateway == nil {
t.Fatal("expected reconcile gateway event")
}
gw := v2.reconcileInput.Event.Gateway
if got, want := gw.StepRef, "hop_2_settlement_observe"; got != want {
t.Fatalf("step_ref mismatch: got=%q want=%q", got, want)
}
if got, want := gw.Status, erecon.GatewayStatusSuccess; got != want {
t.Fatalf("status mismatch: got=%q want=%q", got, want)
}
if got, want := gw.OperationRef, operationRef; got != want {
t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want)
}
if got, want := gw.GatewayInstanceID, "ea2600ce-3de6-4cc5-bd1e-e26ebaceb6b4"; got != want {
t.Fatalf("gateway_instance_id mismatch: got=%q want=%q", got, want)
}
}
var _ prepo.Repository = (*fakeExternalRuntimeRepo)(nil)
var _ psvc.Service = (*fakeExternalRuntimeV2)(nil)