fixed po <-> tgsettle contract
This commit is contained in:
@@ -136,13 +136,22 @@ func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOp
|
|||||||
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
|
||||||
}
|
}
|
||||||
transfer := resp.GetTransfer()
|
transfer := resp.GetTransfer()
|
||||||
|
operationID := strings.TrimSpace(transfer.GetOperationRef())
|
||||||
|
if operationID == "" {
|
||||||
|
s.logger.Warn("Submit operation transfer response missing operation_ref", append(logFields,
|
||||||
|
zap.String("transfer_ref", strings.TrimSpace(transfer.GetTransferRef())),
|
||||||
|
)...)
|
||||||
|
return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{
|
||||||
|
Error: connectorError(connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE, "submit_operation: operation_ref is missing in transfer response", op, ""),
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
s.logger.Info("Submit operation transfer submitted", append(logFields,
|
s.logger.Info("Submit operation transfer submitted", append(logFields,
|
||||||
zap.String("transfer_ref", strings.TrimSpace(transfer.GetTransferRef())),
|
zap.String("transfer_ref", strings.TrimSpace(transfer.GetTransferRef())),
|
||||||
zap.String("status", transfer.GetStatus().String()),
|
zap.String("status", transfer.GetStatus().String()),
|
||||||
)...)
|
)...)
|
||||||
return &connectorv1.SubmitOperationResponse{
|
return &connectorv1.SubmitOperationResponse{
|
||||||
Receipt: &connectorv1.OperationReceipt{
|
Receipt: &connectorv1.OperationReceipt{
|
||||||
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
OperationId: operationID,
|
||||||
Status: transferStatusToOperation(transfer.GetStatus()),
|
Status: transferStatusToOperation(transfer.GetStatus()),
|
||||||
ProviderRef: strings.TrimSpace(transfer.GetTransferRef()),
|
ProviderRef: strings.TrimSpace(transfer.GetTransferRef()),
|
||||||
},
|
},
|
||||||
@@ -224,7 +233,7 @@ func transferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
op := &connectorv1.Operation{
|
op := &connectorv1.Operation{
|
||||||
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
|
OperationId: strings.TrimSpace(transfer.GetOperationRef()),
|
||||||
Type: connectorv1.OperationType_TRANSFER,
|
Type: connectorv1.OperationType_TRANSFER,
|
||||||
Status: transferStatusToOperation(transfer.GetStatus()),
|
Status: transferStatusToOperation(transfer.GetStatus()),
|
||||||
Money: transfer.GetRequestedAmount(),
|
Money: transfer.GetRequestedAmount(),
|
||||||
|
|||||||
119
api/gateway/tgsettle/internal/service/gateway/connector_test.go
Normal file
119
api/gateway/tgsettle/internal/service/gateway/connector_test.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
storagemodel "github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||||
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubmitOperation_UsesOperationRefAsOperationID(t *testing.T) {
|
||||||
|
svc, _, _ := newTestService(t)
|
||||||
|
svc.chatID = "1"
|
||||||
|
|
||||||
|
req := &connectorv1.SubmitOperationRequest{
|
||||||
|
Operation: &connectorv1.Operation{
|
||||||
|
Type: connectorv1.OperationType_TRANSFER,
|
||||||
|
IdempotencyKey: "idem-settlement-1",
|
||||||
|
OperationRef: "payment-1:hop_2_settlement_fx_convert",
|
||||||
|
IntentRef: "intent-1",
|
||||||
|
Money: &moneyv1.Money{Amount: "1.00", Currency: "USDT"},
|
||||||
|
From: &connectorv1.OperationParty{
|
||||||
|
Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: tgsettleConnectorID,
|
||||||
|
AccountId: "wallet-src",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
To: &connectorv1.OperationParty{
|
||||||
|
Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
|
||||||
|
ConnectorId: tgsettleConnectorID,
|
||||||
|
AccountId: "wallet-dst",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Params: structFromMap(map[string]interface{}{
|
||||||
|
"payment_ref": "payment-1",
|
||||||
|
"organization_ref": "org-1",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := svc.SubmitOperation(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SubmitOperation returned error: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetReceipt() == nil {
|
||||||
|
t.Fatal("expected receipt")
|
||||||
|
}
|
||||||
|
if got := resp.GetReceipt().GetError(); got != nil {
|
||||||
|
t.Fatalf("expected no connector error, got: %v", got)
|
||||||
|
}
|
||||||
|
if got, want := resp.GetReceipt().GetOperationId(), "payment-1:hop_2_settlement_fx_convert"; got != want {
|
||||||
|
t.Fatalf("operation_id mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := resp.GetReceipt().GetProviderRef(), "idem-settlement-1"; got != want {
|
||||||
|
t.Fatalf("provider_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOperation_UsesOperationRefIdentity(t *testing.T) {
|
||||||
|
svc, repo, _ := newTestService(t)
|
||||||
|
|
||||||
|
record := &storagemodel.PaymentRecord{
|
||||||
|
IdempotencyKey: "idem-settlement-2",
|
||||||
|
OperationRef: "payment-2:hop_2_settlement_fx_convert",
|
||||||
|
PaymentIntentID: "pi-2",
|
||||||
|
PaymentRef: "payment-2",
|
||||||
|
RequestedMoney: &paymenttypes.Money{Amount: "5.00", Currency: "USDT"},
|
||||||
|
Status: storagemodel.PaymentStatusSuccess,
|
||||||
|
}
|
||||||
|
if err := repo.payments.Upsert(context.Background(), record); err != nil {
|
||||||
|
t.Fatalf("failed to seed payment record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := svc.GetOperation(context.Background(), &connectorv1.GetOperationRequest{
|
||||||
|
OperationId: "payment-2:hop_2_settlement_fx_convert",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetOperation returned error: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetOperation() == nil {
|
||||||
|
t.Fatal("expected operation")
|
||||||
|
}
|
||||||
|
if got, want := resp.GetOperation().GetOperationId(), "payment-2:hop_2_settlement_fx_convert"; got != want {
|
||||||
|
t.Fatalf("operation_id mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := resp.GetOperation().GetProviderRef(), "idem-settlement-2"; got != want {
|
||||||
|
t.Fatalf("provider_ref mismatch: got=%q want=%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOperation_DoesNotResolveByIdempotencyKey(t *testing.T) {
|
||||||
|
svc, repo, _ := newTestService(t)
|
||||||
|
|
||||||
|
record := &storagemodel.PaymentRecord{
|
||||||
|
IdempotencyKey: "idem-settlement-3",
|
||||||
|
OperationRef: "payment-3:hop_2_settlement_fx_convert",
|
||||||
|
PaymentIntentID: "pi-3",
|
||||||
|
PaymentRef: "payment-3",
|
||||||
|
RequestedMoney: &paymenttypes.Money{Amount: "5.00", Currency: "USDT"},
|
||||||
|
Status: storagemodel.PaymentStatusSuccess,
|
||||||
|
}
|
||||||
|
if err := repo.payments.Upsert(context.Background(), record); err != nil {
|
||||||
|
t.Fatalf("failed to seed payment record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := svc.GetOperation(context.Background(), &connectorv1.GetOperationRequest{
|
||||||
|
OperationId: "idem-settlement-3",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected not found error")
|
||||||
|
}
|
||||||
|
if status.Code(err) != codes.NotFound {
|
||||||
|
t.Fatalf("unexpected error code: got=%s want=%s", status.Code(err), codes.NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,22 +20,25 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PaymentRecord struct {
|
type PaymentRecord struct {
|
||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
OperationRef string `bson:"operationRef,omitempty" json:"operation_ref,omitempty"`
|
OperationRef string `bson:"operationRef,omitempty" json:"operation_ref,omitempty"`
|
||||||
IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotency_key,omitempty"`
|
IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotency_key,omitempty"`
|
||||||
PaymentIntentID string `bson:"paymentIntentId,omitempty" json:"payment_intent_id,omitempty"`
|
ConfirmationRef string `bson:"confirmationRef,omitempty" json:"confirmation_ref,omitempty"`
|
||||||
QuoteRef string `bson:"quoteRef,omitempty" json:"quote_ref,omitempty"`
|
ConfirmationMessageID string `bson:"confirmationMessageId,omitempty" json:"confirmation_message_id,omitempty"`
|
||||||
IntentRef string `bson:"intentRef,omitempty" json:"intent_ref,omitempty"`
|
ConfirmationReplyMessageID string `bson:"confirmationReplyMessageId,omitempty" json:"confirmation_reply_message_id,omitempty"`
|
||||||
PaymentRef string `bson:"paymentRef,omitempty" json:"payment_ref,omitempty"`
|
PaymentIntentID string `bson:"paymentIntentId,omitempty" json:"payment_intent_id,omitempty"`
|
||||||
OutgoingLeg string `bson:"outgoingLeg,omitempty" json:"outgoing_leg,omitempty"`
|
QuoteRef string `bson:"quoteRef,omitempty" json:"quote_ref,omitempty"`
|
||||||
TargetChatID string `bson:"targetChatId,omitempty" json:"target_chat_id,omitempty"`
|
IntentRef string `bson:"intentRef,omitempty" json:"intent_ref,omitempty"`
|
||||||
RequestedMoney *paymenttypes.Money `bson:"requestedMoney,omitempty" json:"requested_money,omitempty"`
|
PaymentRef string `bson:"paymentRef,omitempty" json:"payment_ref,omitempty"`
|
||||||
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executed_money,omitempty"`
|
OutgoingLeg string `bson:"outgoingLeg,omitempty" json:"outgoing_leg,omitempty"`
|
||||||
Status PaymentStatus `bson:"status,omitempty" json:"status,omitempty"`
|
TargetChatID string `bson:"targetChatId,omitempty" json:"target_chat_id,omitempty"`
|
||||||
FailureReason string `bson:"failureReason,omitempty" json:"Failure_reason,omitempty"`
|
RequestedMoney *paymenttypes.Money `bson:"requestedMoney,omitempty" json:"requested_money,omitempty"`
|
||||||
ExecutedAt time.Time `bson:"executedAt,omitempty" json:"executed_at,omitempty"`
|
ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executed_money,omitempty"`
|
||||||
ExpiresAt time.Time `bson:"expiresAt,omitempty" json:"expires_at,omitempty"`
|
Status PaymentStatus `bson:"status,omitempty" json:"status,omitempty"`
|
||||||
ExpiredAt time.Time `bson:"expiredAt,omitempty" json:"expired_at,omitempty"`
|
FailureReason string `bson:"failureReason,omitempty" json:"Failure_reason,omitempty"`
|
||||||
|
ExecutedAt time.Time `bson:"executedAt,omitempty" json:"executed_at,omitempty"`
|
||||||
|
ExpiresAt time.Time `bson:"expiresAt,omitempty" json:"expires_at,omitempty"`
|
||||||
|
ExpiredAt time.Time `bson:"expiredAt,omitempty" json:"expired_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramConfirmation struct {
|
type TelegramConfirmation struct {
|
||||||
|
|||||||
@@ -103,14 +103,13 @@ func (p *Payments) Upsert(ctx context.Context, record *model.PaymentRecord) erro
|
|||||||
return merrors.InvalidArgument("payment record is nil", "record")
|
return merrors.InvalidArgument("payment record is nil", "record")
|
||||||
}
|
}
|
||||||
record.IdempotencyKey = strings.TrimSpace(record.IdempotencyKey)
|
record.IdempotencyKey = strings.TrimSpace(record.IdempotencyKey)
|
||||||
record.PaymentIntentID = strings.TrimSpace(record.PaymentIntentID)
|
|
||||||
record.QuoteRef = strings.TrimSpace(record.QuoteRef)
|
record.QuoteRef = strings.TrimSpace(record.QuoteRef)
|
||||||
record.OutgoingLeg = strings.TrimSpace(record.OutgoingLeg)
|
record.OutgoingLeg = strings.TrimSpace(record.OutgoingLeg)
|
||||||
record.TargetChatID = strings.TrimSpace(record.TargetChatID)
|
record.TargetChatID = strings.TrimSpace(record.TargetChatID)
|
||||||
record.IntentRef = strings.TrimSpace(record.IntentRef)
|
record.IntentRef = strings.TrimSpace(record.IntentRef)
|
||||||
record.OperationRef = strings.TrimSpace(record.OperationRef)
|
record.OperationRef = strings.TrimSpace(record.OperationRef)
|
||||||
if record.PaymentIntentID == "" {
|
if record.IntentRef == "" {
|
||||||
return merrors.InvalidArgument("intention reference is required", "payment_intent_ref")
|
return merrors.InvalidArgument("intention reference is required", "intent_ref")
|
||||||
}
|
}
|
||||||
if record.IdempotencyKey == "" {
|
if record.IdempotencyKey == "" {
|
||||||
return merrors.InvalidArgument("idempotency key is required", "idempotency_key")
|
return merrors.InvalidArgument("idempotency key is required", "idempotency_key")
|
||||||
@@ -148,7 +147,7 @@ func (p *Payments) Upsert(ctx context.Context, record *model.PaymentRecord) erro
|
|||||||
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||||
p.logger.Warn("Failed to upsert payment record",
|
p.logger.Warn("Failed to upsert payment record",
|
||||||
zap.String("idempotency_key", record.IdempotencyKey),
|
zap.String("idempotency_key", record.IdempotencyKey),
|
||||||
zap.String("payment_intent_id", record.PaymentIntentID),
|
zap.String("intent_ref", record.IntentRef),
|
||||||
zap.String("quote_ref", record.QuoteRef),
|
zap.String("quote_ref", record.QuoteRef),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,9 +112,8 @@ func TestPaymentsUpsert_ReusesExistingIDFromIdempotencyLookup(t *testing.T) {
|
|||||||
CreatedAt: existingCreatedAt,
|
CreatedAt: existingCreatedAt,
|
||||||
UpdatedAt: existingCreatedAt,
|
UpdatedAt: existingCreatedAt,
|
||||||
},
|
},
|
||||||
IdempotencyKey: key,
|
IdempotencyKey: key,
|
||||||
PaymentIntentID: "pi-old",
|
IntentRef: "pi-old",
|
||||||
IntentRef: "intent-old",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
duplicateWhenZeroID: true,
|
duplicateWhenZeroID: true,
|
||||||
@@ -122,10 +121,9 @@ func TestPaymentsUpsert_ReusesExistingIDFromIdempotencyLookup(t *testing.T) {
|
|||||||
store := &Payments{logger: zap.NewNop(), repo: repo}
|
store := &Payments{logger: zap.NewNop(), repo: repo}
|
||||||
|
|
||||||
record := &model.PaymentRecord{
|
record := &model.PaymentRecord{
|
||||||
IdempotencyKey: key,
|
IdempotencyKey: key,
|
||||||
PaymentIntentID: "pi-new",
|
IntentRef: "pi-new",
|
||||||
QuoteRef: "quote-new",
|
QuoteRef: "quote-new",
|
||||||
IntentRef: "intent-new",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.Upsert(context.Background(), record); err != nil {
|
if err := store.Upsert(context.Background(), record); err != nil {
|
||||||
@@ -155,9 +153,8 @@ func TestPaymentsUpsert_RetriesAfterDuplicateKeyRace(t *testing.T) {
|
|||||||
CreatedAt: time.Date(2026, 3, 6, 10, 1, 0, 0, time.UTC),
|
CreatedAt: time.Date(2026, 3, 6, 10, 1, 0, 0, time.UTC),
|
||||||
UpdatedAt: time.Date(2026, 3, 6, 10, 1, 0, 0, time.UTC),
|
UpdatedAt: time.Date(2026, 3, 6, 10, 1, 0, 0, time.UTC),
|
||||||
},
|
},
|
||||||
IdempotencyKey: key,
|
IdempotencyKey: key,
|
||||||
PaymentIntentID: "pi-existing",
|
IntentRef: "pi-existing",
|
||||||
IntentRef: "intent-existing",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
findErrByCall: map[int]error{
|
findErrByCall: map[int]error{
|
||||||
@@ -168,10 +165,9 @@ func TestPaymentsUpsert_RetriesAfterDuplicateKeyRace(t *testing.T) {
|
|||||||
store := &Payments{logger: zap.NewNop(), repo: repo}
|
store := &Payments{logger: zap.NewNop(), repo: repo}
|
||||||
|
|
||||||
record := &model.PaymentRecord{
|
record := &model.PaymentRecord{
|
||||||
IdempotencyKey: key,
|
IdempotencyKey: key,
|
||||||
PaymentIntentID: "pi-new",
|
IntentRef: "pi-new",
|
||||||
QuoteRef: "quote-new",
|
QuoteRef: "quote-new",
|
||||||
IntentRef: "intent-new",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.Upsert(context.Background(), record); err != nil {
|
if err := store.Upsert(context.Background(), record); err != nil {
|
||||||
@@ -203,9 +199,8 @@ func TestPaymentsUpsert_PropagatesNoSuchTransactionAfterDuplicate(t *testing.T)
|
|||||||
CreatedAt: time.Date(2026, 3, 6, 10, 2, 0, 0, time.UTC),
|
CreatedAt: time.Date(2026, 3, 6, 10, 2, 0, 0, time.UTC),
|
||||||
UpdatedAt: time.Date(2026, 3, 6, 10, 2, 0, 0, time.UTC),
|
UpdatedAt: time.Date(2026, 3, 6, 10, 2, 0, 0, time.UTC),
|
||||||
},
|
},
|
||||||
IdempotencyKey: key,
|
IdempotencyKey: key,
|
||||||
PaymentIntentID: "pi-existing",
|
IntentRef: "pi-existing",
|
||||||
IntentRef: "intent-existing",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
findErrByCall: map[int]error{
|
findErrByCall: map[int]error{
|
||||||
@@ -221,10 +216,9 @@ func TestPaymentsUpsert_PropagatesNoSuchTransactionAfterDuplicate(t *testing.T)
|
|||||||
store := &Payments{logger: zap.NewNop(), repo: repo}
|
store := &Payments{logger: zap.NewNop(), repo: repo}
|
||||||
|
|
||||||
record := &model.PaymentRecord{
|
record := &model.PaymentRecord{
|
||||||
IdempotencyKey: key,
|
IdempotencyKey: key,
|
||||||
PaymentIntentID: "pi-new",
|
IntentRef: "pi-new",
|
||||||
QuoteRef: "quote-new",
|
QuoteRef: "quote-new",
|
||||||
IntentRef: "intent-new",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := store.Upsert(context.Background(), record)
|
err := store.Upsert(context.Background(), record)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ require (
|
|||||||
github.com/tech/sendico/pkg v0.1.0
|
github.com/tech/sendico/pkg v0.1.0
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||||
go.uber.org/zap v1.27.1
|
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
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
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.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
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 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ const (
|
|||||||
type StepShell struct {
|
type StepShell struct {
|
||||||
StepRef string `bson:"stepRef" json:"stepRef"`
|
StepRef string `bson:"stepRef" json:"stepRef"`
|
||||||
StepCode string `bson:"stepCode" json:"stepCode"`
|
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"`
|
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||||
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
|
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -53,6 +56,9 @@ type StepShell struct {
|
|||||||
type StepExecution struct {
|
type StepExecution struct {
|
||||||
StepRef string `bson:"stepRef" json:"stepRef"`
|
StepRef string `bson:"stepRef" json:"stepRef"`
|
||||||
StepCode string `bson:"stepCode" json:"stepCode"`
|
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"`
|
ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"`
|
||||||
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
|
UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"`
|
||||||
State StepState `bson:"state" json:"state"`
|
State StepState `bson:"state" json:"state"`
|
||||||
|
|||||||
@@ -143,10 +143,16 @@ func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
|
|||||||
return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].report_visibility is invalid")
|
return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].report_visibility is invalid")
|
||||||
}
|
}
|
||||||
userLabel := strings.TrimSpace(shell[i].UserLabel)
|
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{
|
out = append(out, StepExecution{
|
||||||
StepRef: stepRef,
|
StepRef: stepRef,
|
||||||
StepCode: stepCode,
|
StepCode: stepCode,
|
||||||
|
Rail: railValue,
|
||||||
|
Gateway: gatewayID,
|
||||||
|
InstanceID: instanceID,
|
||||||
ReportVisibility: visibility,
|
ReportVisibility: visibility,
|
||||||
UserLabel: userLabel,
|
UserLabel: userLabel,
|
||||||
State: StepStatePending,
|
State: StepStatePending,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/payments/storage/model"
|
"github.com/tech/sendico/payments/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
@@ -42,7 +43,15 @@ func TestCreate_OK(t *testing.T) {
|
|||||||
QuoteSnapshot: quote,
|
QuoteSnapshot: quote,
|
||||||
Steps: []StepShell{
|
Steps: []StepShell{
|
||||||
{StepRef: " s1 ", StepCode: " reserve_funds ", ReportVisibility: model.ReportVisibilityHidden},
|
{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 {
|
if err != nil {
|
||||||
@@ -111,6 +120,15 @@ func TestCreate_OK(t *testing.T) {
|
|||||||
if got, want := payment.StepExecutions[1].UserLabel, "Card payout"; got != want {
|
if got, want := payment.StepExecutions[1].UserLabel, "Card payout"; got != want {
|
||||||
t.Fatalf("unexpected second step user label: got=%q want=%q", 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.
|
// Verify immutable snapshot semantics by ensuring clones were created.
|
||||||
payment.IntentSnapshot.Ref = "changed"
|
payment.IntentSnapshot.Ref = "changed"
|
||||||
|
|||||||
@@ -221,6 +221,9 @@ func toStepShells(graph *xplan.Graph) []agg.StepShell {
|
|||||||
out = append(out, agg.StepShell{
|
out = append(out, agg.StepShell{
|
||||||
StepRef: graph.Steps[i].StepRef,
|
StepRef: graph.Steps[i].StepRef,
|
||||||
StepCode: graph.Steps[i].StepCode,
|
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,
|
ReportVisibility: graph.Steps[i].Visibility,
|
||||||
UserLabel: graph.Steps[i].UserLabel,
|
UserLabel: graph.Steps[i].UserLabel,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -408,6 +408,15 @@ func stepExecutionEqual(left, right agg.StepExecution) bool {
|
|||||||
if left.StepRef != right.StepRef || left.StepCode != right.StepCode {
|
if left.StepRef != right.StepRef || left.StepCode != right.StepCode {
|
||||||
return false
|
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 {
|
if left.State != right.State || left.Attempt != right.Attempt {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
|
"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/orchestrator/internal/service/orchestrationv2/xplan"
|
||||||
"github.com/tech/sendico/payments/storage/model"
|
"github.com/tech/sendico/payments/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +145,16 @@ func (s *svc) normalizeStepExecutions(
|
|||||||
stepCode = stepsByRef[stepRef].StepCode
|
stepCode = stepsByRef[stepRef].StepCode
|
||||||
}
|
}
|
||||||
exec.StepCode = 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.ReportVisibility = effectiveStepVisibility(exec.ReportVisibility, stepsByRef[stepRef].Visibility)
|
||||||
exec.UserLabel = firstNonEmpty(exec.UserLabel, stepsByRef[stepRef].UserLabel)
|
exec.UserLabel = firstNonEmpty(exec.UserLabel, stepsByRef[stepRef].UserLabel)
|
||||||
cloned := cloneStepExecution(exec)
|
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.FailureCode = strings.TrimSpace(exec.FailureCode)
|
||||||
exec.FailureMsg = strings.TrimSpace(exec.FailureMsg)
|
exec.FailureMsg = strings.TrimSpace(exec.FailureMsg)
|
||||||
exec.UserLabel = strings.TrimSpace(exec.UserLabel)
|
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.ReportVisibility = model.NormalizeReportVisibility(exec.ReportVisibility)
|
||||||
exec.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
|
exec.ExternalRefs = cloneExternalRefs(exec.ExternalRefs)
|
||||||
if exec.StepRef == "" {
|
if exec.StepRef == "" {
|
||||||
@@ -197,6 +211,9 @@ func seedMissingExecutions(
|
|||||||
executionsByRef[stepRef] = &agg.StepExecution{
|
executionsByRef[stepRef] = &agg.StepExecution{
|
||||||
StepRef: step.StepRef,
|
StepRef: step.StepRef,
|
||||||
StepCode: step.StepCode,
|
StepCode: step.StepCode,
|
||||||
|
Rail: step.Rail,
|
||||||
|
Gateway: strings.TrimSpace(step.Gateway),
|
||||||
|
InstanceID: strings.TrimSpace(step.InstanceID),
|
||||||
ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
|
ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility),
|
||||||
UserLabel: strings.TrimSpace(step.UserLabel),
|
UserLabel: strings.TrimSpace(step.UserLabel),
|
||||||
State: agg.StepStatePending,
|
State: agg.StepStatePending,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
|
"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/prepo"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
|
"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"
|
"github.com/tech/sendico/payments/storage/model"
|
||||||
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
cons "github.com/tech/sendico/pkg/messaging/consumer"
|
||||||
paymentgatewaynotifications "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
|
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 == "" {
|
if candidate.stepRef == "" || candidate.transferRef == "" {
|
||||||
return runningObserveCandidate{}, false
|
return runningObserveCandidate{}, false
|
||||||
}
|
}
|
||||||
@@ -475,7 +477,7 @@ func (s *Service) pollObserveCandidate(ctx context.Context, payment *agg.Payment
|
|||||||
StepRef: candidate.stepRef,
|
StepRef: candidate.stepRef,
|
||||||
OperationRef: firstNonEmpty(strings.TrimSpace(transfer.GetOperationRef()), candidate.operationRef),
|
OperationRef: firstNonEmpty(strings.TrimSpace(transfer.GetOperationRef()), candidate.operationRef),
|
||||||
TransferRef: strings.TrimSpace(candidate.transferRef),
|
TransferRef: strings.TrimSpace(candidate.transferRef),
|
||||||
GatewayInstanceID: firstNonEmpty(candidate.gatewayInstanceID, strings.TrimSpace(gateway.InstanceID), strings.TrimSpace(gateway.ID)),
|
GatewayInstanceID: resolvedObserveGatewayID(candidate.gatewayInstanceID, gateway),
|
||||||
Status: status,
|
Status: status,
|
||||||
}
|
}
|
||||||
switch 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) {
|
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 != "" {
|
if gatewayID := strings.TrimSpace(candidate.gatewayInstanceID); gatewayID != "" {
|
||||||
items, err := s.gatewayRegistry.List(ctx)
|
if item := findEnabledGatewayDescriptor(items, gatewayID, expectedRail); item != nil {
|
||||||
if err == nil {
|
return item, 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 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{
|
type observeStepHint struct {
|
||||||
gatewayRegistry: s.gatewayRegistry,
|
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{
|
key := strings.TrimSpace(stepRef)
|
||||||
Rail: discovery.RailCrypto,
|
if key == "" {
|
||||||
|
return observeStepHint{}, false
|
||||||
}
|
}
|
||||||
if gatewayID := strings.TrimSpace(candidate.gatewayInstanceID); gatewayID != "" {
|
for i := range payment.StepExecutions {
|
||||||
step.InstanceID = gatewayID
|
step := payment.StepExecutions[i]
|
||||||
step.Gateway = gatewayID
|
if !strings.EqualFold(strings.TrimSpace(step.StepRef), key) {
|
||||||
} else if gateway, instanceID, ok := sourceCryptoHop(payment); ok {
|
continue
|
||||||
step.Gateway = strings.TrimSpace(gateway)
|
}
|
||||||
step.InstanceID = strings.TrimSpace(instanceID)
|
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) {
|
func mapTransferStatus(status chainv1.TransferStatus) (gatewayStatus erecon.GatewayStatus, terminal bool, ok bool) {
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ package orchestrator
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/tech/sendico/pkg/discovery"
|
|
||||||
"testing"
|
"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/agg"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon"
|
"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/prepo"
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
|
||||||
"github.com/tech/sendico/payments/storage/model"
|
"github.com/tech/sendico/payments/storage/model"
|
||||||
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
pm "github.com/tech/sendico/pkg/model"
|
pm "github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
paymenttypes "github.com/tech/sendico/pkg/payments/types"
|
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) {
|
func TestResolveObserveGateway_UsesExternalRefGatewayInstanceAcrossRails(t *testing.T) {
|
||||||
svc := &Service{
|
svc := &Service{
|
||||||
gatewayRegistry: &fakeGatewayRegistry{
|
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 _ prepo.Repository = (*fakeExternalRuntimeRepo)(nil)
|
||||||
var _ psvc.Service = (*fakeExternalRuntimeV2)(nil)
|
var _ psvc.Service = (*fakeExternalRuntimeV2)(nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user