document generation for ops

This commit is contained in:
Stephan D
2026-03-04 15:41:28 +01:00
parent 80b25a8608
commit 92a6191014
21 changed files with 1077 additions and 255 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/gateway/chain/internal/appversion"
"github.com/tech/sendico/gateway/chain/internal/service/gateway/shared"
chainstoragemodel "github.com/tech/sendico/gateway/chain/storage/model"
chainasset "github.com/tech/sendico/pkg/chain"
"github.com/tech/sendico/pkg/connector/params"
"github.com/tech/sendico/pkg/merrors"
@@ -17,6 +18,7 @@ import (
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
const chainConnectorID = "chain"
@@ -293,11 +295,21 @@ func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperatio
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
}
resp, err := s.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: strings.TrimSpace(req.GetOperationId())})
operationRef := strings.TrimSpace(req.GetOperationId())
if s.storage == nil || s.storage.Transfers() == nil {
return nil, merrors.Internal("get_operation: storage is not configured")
}
transfer, err := s.storage.Transfers().FindByOperationRef(ctx, "", operationRef)
if err != nil {
return nil, err
}
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(resp.GetTransfer())}, nil
if transfer == nil {
return nil, merrors.NoData("transfer not found")
}
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(storageTransferToProto(transfer))}, nil
}
func (s *Service) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
@@ -493,6 +505,61 @@ func feeEstimateResult(resp *chainv1.EstimateTransferFeeResponse) *structpb.Stru
return result
}
func storageTransferToProto(transfer *chainstoragemodel.Transfer) *chainv1.Transfer {
if transfer == nil {
return nil
}
destination := &chainv1.TransferDestination{Memo: strings.TrimSpace(transfer.Destination.Memo)}
if managedWalletRef := strings.TrimSpace(transfer.Destination.ManagedWalletRef); managedWalletRef != "" {
destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: managedWalletRef}
} else if externalAddress := strings.TrimSpace(transfer.Destination.ExternalAddress); externalAddress != "" {
destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: externalAddress}
}
fees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
for _, fee := range transfer.Fees {
fees = append(fees, &chainv1.ServiceFeeBreakdown{
FeeCode: strings.TrimSpace(fee.FeeCode),
Amount: fee.Amount,
Description: strings.TrimSpace(fee.Description),
})
}
asset := &chainv1.Asset{
Chain: shared.ChainEnumFromName(transfer.Network),
TokenSymbol: strings.TrimSpace(transfer.TokenSymbol),
ContractAddress: strings.TrimSpace(transfer.ContractAddress),
}
protoTransfer := &chainv1.Transfer{
TransferRef: strings.TrimSpace(transfer.TransferRef),
IdempotencyKey: strings.TrimSpace(transfer.IdempotencyKey),
IntentRef: strings.TrimSpace(transfer.IntentRef),
OperationRef: strings.TrimSpace(transfer.OperationRef),
OrganizationRef: strings.TrimSpace(transfer.OrganizationRef),
SourceWalletRef: strings.TrimSpace(transfer.SourceWalletRef),
Destination: destination,
Asset: asset,
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
NetAmount: shared.MonenyToProto(transfer.NetAmount),
Fees: fees,
Status: shared.TransferStatusToProto(transfer.Status),
TransactionHash: strings.TrimSpace(transfer.TxHash),
FailureReason: strings.TrimSpace(transfer.FailureReason),
PaymentRef: strings.TrimSpace(transfer.PaymentRef),
}
if !transfer.CreatedAt.IsZero() {
protoTransfer.CreatedAt = timestamppb.New(transfer.CreatedAt.UTC())
}
if !transfer.UpdatedAt.IsZero() {
protoTransfer.UpdatedAt = timestamppb.New(transfer.UpdatedAt.UTC())
}
return protoTransfer
}
func gasTopUpResult(amount *moneyv1.Money, capHit bool, transferRef string) *structpb.Struct {
payload := map[string]interface{}{
"cap_hit": capHit,
@@ -518,18 +585,33 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
return nil
}
op := &connectorv1.Operation{
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
Type: connectorv1.OperationType_TRANSFER,
Status: chainTransferStatusToOperation(transfer.GetStatus()),
Money: transfer.GetRequestedAmount(),
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
CreatedAt: transfer.GetCreatedAt(),
UpdatedAt: transfer.GetUpdatedAt(),
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
Type: connectorv1.OperationType_TRANSFER,
Status: chainTransferStatusToOperation(transfer.GetStatus()),
Money: transfer.GetRequestedAmount(),
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
IntentRef: strings.TrimSpace(transfer.GetIntentRef()),
OperationRef: strings.TrimSpace(transfer.GetOperationRef()),
CreatedAt: transfer.GetCreatedAt(),
UpdatedAt: transfer.GetUpdatedAt(),
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
ConnectorId: chainConnectorID,
AccountId: strings.TrimSpace(transfer.GetSourceWalletRef()),
}}},
}
params := map[string]interface{}{}
if paymentRef := strings.TrimSpace(transfer.GetPaymentRef()); paymentRef != "" {
params["payment_ref"] = paymentRef
}
if organizationRef := strings.TrimSpace(transfer.GetOrganizationRef()); organizationRef != "" {
params["organization_ref"] = organizationRef
}
if failureReason := strings.TrimSpace(transfer.GetFailureReason()); failureReason != "" {
params["failure_reason"] = failureReason
}
if len(params) > 0 {
op.Params = structFromMap(params)
}
if dest := transfer.GetDestination(); dest != nil {
switch d := dest.GetDestination().(type) {
case *chainv1.TransferDestination_ManagedWalletRef:
@@ -629,6 +711,17 @@ func operationAccountID(party *connectorv1.OperationParty) string {
return ""
}
func structFromMap(values map[string]interface{}) *structpb.Struct {
if len(values) == 0 {
return nil
}
result, err := structpb.NewStruct(values)
if err != nil {
return nil
}
return result
}
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
err := &connectorv1.ConnectorError{
Code: code,

View File

@@ -500,6 +500,32 @@ func (t *inMemoryTransfers) Get(ctx context.Context, transferRef string) (*model
return transfer, nil
}
func (t *inMemoryTransfers) FindByOperationRef(ctx context.Context, organizationRef, operationRef string) (*model.Transfer, error) {
t.mu.Lock()
defer t.mu.Unlock()
org := strings.TrimSpace(organizationRef)
opRef := strings.TrimSpace(operationRef)
if opRef == "" {
return nil, merrors.InvalidArgument("transfersStore: empty operationRef")
}
for _, transfer := range t.items {
if transfer == nil {
continue
}
if !strings.EqualFold(strings.TrimSpace(transfer.OperationRef), opRef) {
continue
}
if org != "" && !strings.EqualFold(strings.TrimSpace(transfer.OrganizationRef), org) {
continue
}
return transfer, nil
}
return nil, merrors.NoData("transfer not found")
}
func (t *inMemoryTransfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) {
t.mu.Lock()
defer t.mu.Unlock()

View File

@@ -40,6 +40,9 @@ func NewTransfers(logger mlogger.Logger, db *mongo.Database) (*Transfers, error)
Keys: []ri.Key{{Field: "transferRef", Sort: ri.Asc}},
Unique: true,
},
{
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}, {Field: "operationRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}},
Unique: true,
@@ -110,6 +113,25 @@ func (t *Transfers) Get(ctx context.Context, transferRef string) (*model.Transfe
return transfer, nil
}
func (t *Transfers) FindByOperationRef(ctx context.Context, organizationRef, operationRef string) (*model.Transfer, error) {
operationRef = strings.TrimSpace(operationRef)
if operationRef == "" {
return nil, merrors.InvalidArgument("transfersStore: empty operationRef")
}
query := repository.Query().Filter(repository.Field("operationRef"), operationRef)
if org := strings.TrimSpace(organizationRef); org != "" {
query = query.Filter(repository.Field("organizationRef"), org)
}
transfer := &model.Transfer{}
if err := t.repo.FindOneByFilter(ctx, query, transfer); err != nil {
return nil, err
}
return transfer, nil
}
func (t *Transfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) {
query := repository.Query()
if src := strings.TrimSpace(filter.SourceWalletRef); src != "" {

View File

@@ -42,6 +42,7 @@ type WalletsStore interface {
type TransfersStore interface {
Create(ctx context.Context, transfer *model.Transfer) (*model.Transfer, error)
Get(ctx context.Context, transferRef string) (*model.Transfer, error)
FindByOperationRef(ctx context.Context, organizationRef, operationRef string) (*model.Transfer, error)
List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error)
UpdateStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason string, txHash string) (*model.Transfer, error)
}

View File

@@ -12,6 +12,7 @@ import (
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
"google.golang.org/protobuf/types/known/structpb"
)
const mntxConnectorID = "mntx"
@@ -92,11 +93,21 @@ func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperatio
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
}
resp, err := s.GetCardPayoutStatus(ctx, &mntxv1.GetCardPayoutStatusRequest{PayoutId: strings.TrimSpace(req.GetOperationId())})
operationRef := strings.TrimSpace(req.GetOperationId())
if s.storage == nil || s.storage.Payouts() == nil {
return nil, merrors.Internal("get_operation: storage is not configured")
}
payout, err := s.storage.Payouts().FindByOperationRef(ctx, operationRef)
if err != nil {
return nil, err
}
return &connectorv1.GetOperationResponse{Operation: payoutToOperation(resp.GetPayout())}, nil
if payout == nil {
return nil, merrors.NoData("payout not found")
}
return &connectorv1.GetOperationResponse{Operation: payoutToOperation(StateToProto(payout))}, nil
}
func (s *Service) ListOperations(_ context.Context, _ *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
@@ -274,7 +285,7 @@ func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
if state == nil {
return nil
}
return &connectorv1.Operation{
op := &connectorv1.Operation{
OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
Type: connectorv1.OperationType_PAYOUT,
Status: payoutStatusToOperation(state.GetStatus()),
@@ -282,10 +293,30 @@ func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
Amount: minorToDecimal(state.GetAmountMinor()),
Currency: strings.ToUpper(strings.TrimSpace(state.GetCurrency())),
},
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
CreatedAt: state.GetCreatedAt(),
UpdatedAt: state.GetUpdatedAt(),
ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
IntentRef: strings.TrimSpace(state.GetIntentRef()),
OperationRef: strings.TrimSpace(state.GetOperationRef()),
CreatedAt: state.GetCreatedAt(),
UpdatedAt: state.GetUpdatedAt(),
}
params := map[string]interface{}{}
if paymentRef := strings.TrimSpace(state.GetParentPaymentRef()); paymentRef != "" {
params["payment_ref"] = paymentRef
params["parent_payment_ref"] = paymentRef
}
if providerCode := strings.TrimSpace(state.GetProviderCode()); providerCode != "" {
params["provider_code"] = providerCode
}
if providerMessage := strings.TrimSpace(state.GetProviderMessage()); providerMessage != "" {
params["provider_message"] = providerMessage
params["failure_reason"] = providerMessage
}
if len(params) > 0 {
op.Params = structFromMap(params)
}
return op
}
func minorToDecimal(amount int64) string {
@@ -316,6 +347,17 @@ func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationSt
}
}
func structFromMap(values map[string]interface{}) *structpb.Struct {
if len(values) == 0 {
return nil
}
result, err := structpb.NewStruct(values)
if err != nil {
return nil
}
return result
}
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
err := &connectorv1.ConnectorError{
Code: code,

View File

@@ -11,6 +11,9 @@ import (
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
const tgsettleConnectorID = "tgsettle"
@@ -152,12 +155,22 @@ func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperatio
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
}
operationID := strings.TrimSpace(req.GetOperationId())
resp, err := s.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: operationID})
if s.repo == nil || s.repo.Payments() == nil {
s.logger.Warn("Get operation storage unavailable", zap.String("operation_id", operationID))
return nil, merrors.Internal("get_operation: storage is not configured")
}
record, err := s.repo.Payments().FindByOperationRef(ctx, operationID)
if err != nil {
s.logger.Warn("Get operation failed", zap.String("operation_id", operationID), zap.Error(err))
s.logger.Warn("Get operation lookup by operation_ref failed", zap.String("operation_id", operationID), zap.Error(err))
return nil, err
}
return &connectorv1.GetOperationResponse{Operation: transferToOperation(resp.GetTransfer())}, nil
if record == nil {
return nil, status.Error(codes.NotFound, "operation not found")
}
return &connectorv1.GetOperationResponse{Operation: transferToOperation(transferFromPayment(record, nil))}, nil
}
func (s *Service) ListOperations(_ context.Context, _ *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
@@ -221,6 +234,19 @@ func transferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
CreatedAt: transfer.GetCreatedAt(),
UpdatedAt: transfer.GetUpdatedAt(),
}
params := map[string]interface{}{}
if paymentRef := strings.TrimSpace(transfer.GetPaymentRef()); paymentRef != "" {
params["payment_ref"] = paymentRef
}
if organizationRef := strings.TrimSpace(transfer.GetOrganizationRef()); organizationRef != "" {
params["organization_ref"] = organizationRef
}
if failureReason := strings.TrimSpace(transfer.GetFailureReason()); failureReason != "" {
params["failure_reason"] = failureReason
}
if len(params) > 0 {
op.Params = structFromMap(params)
}
if source := strings.TrimSpace(transfer.GetSourceWalletRef()); source != "" {
op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
ConnectorId: tgsettleConnectorID,
@@ -281,6 +307,17 @@ func operationAccountID(party *connectorv1.OperationParty) string {
return ""
}
func structFromMap(values map[string]interface{}) *structpb.Struct {
if len(values) == 0 {
return nil
}
result, err := structpb.NewStruct(values)
if err != nil {
return nil
}
return result
}
func operationLogFields(op *connectorv1.Operation) []zap.Field {
if op == nil {
return nil

View File

@@ -675,6 +675,9 @@ func transferFromRequest(req *chainv1.SubmitTransferRequest) *chainv1.Transfer {
SourceWalletRef: strings.TrimSpace(req.GetSourceWalletRef()),
Destination: req.GetDestination(),
RequestedAmount: req.GetAmount(),
IntentRef: strings.TrimSpace(req.GetIntentRef()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
Status: chainv1.TransferStatus_TRANSFER_CREATED,
}
}
@@ -714,6 +717,10 @@ func transferFromPayment(record *storagemodel.PaymentRecord, req *chainv1.Submit
IdempotencyKey: strings.TrimSpace(record.IdempotencyKey),
RequestedAmount: requested,
NetAmount: net,
IntentRef: strings.TrimSpace(record.IntentRef),
OperationRef: strings.TrimSpace(record.OperationRef),
PaymentRef: strings.TrimSpace(record.PaymentRef),
FailureReason: strings.TrimSpace(record.FailureReason),
Status: status,
}

View File

@@ -37,6 +37,20 @@ func (f *fakePaymentsStore) FindByIdempotencyKey(_ context.Context, key string)
return f.records[key], nil
}
func (f *fakePaymentsStore) FindByOperationRef(_ context.Context, key string) (*storagemodel.PaymentRecord, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.records == nil {
return nil, nil
}
for _, record := range f.records {
if record != nil && record.OperationRef == key {
return record, nil
}
}
return nil, nil
}
func (f *fakePaymentsStore) Upsert(_ context.Context, record *storagemodel.PaymentRecord) error {
f.mu.Lock()
defer f.mu.Unlock()

View File

@@ -20,6 +20,7 @@ import (
const (
paymentsCollection = "payments"
fieldIdempotencyKey = "idempotencyKey"
fieldOperationRef = "operationRef"
)
type Payments struct {
@@ -44,6 +45,14 @@ func NewPayments(logger mlogger.Logger, db *mongo.Database) (*Payments, error) {
logger.Error("Failed to create payments idempotency index", zap.Error(err), zap.String("index_field", fieldIdempotencyKey))
return nil, err
}
if err := repo.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: fieldOperationRef, Sort: ri.Asc}},
Unique: true,
Sparse: true,
}); err != nil {
logger.Error("Failed to create payments operation index", zap.Error(err), zap.String("index_field", fieldOperationRef))
return nil, err
}
p := &Payments{
logger: logger,
@@ -72,6 +81,25 @@ func (p *Payments) FindByIdempotencyKey(ctx context.Context, key string) (*model
return &result, nil
}
func (p *Payments) FindByOperationRef(ctx context.Context, key string) (*model.PaymentRecord, error) {
key = strings.TrimSpace(key)
if key == "" {
return nil, merrors.InvalidArgument("operation reference is required", "operation_ref")
}
var result model.PaymentRecord
err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldOperationRef, key), &result)
if errors.Is(err, merrors.ErrNoData) {
return nil, nil
}
if err != nil {
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
p.logger.Warn("Payment record lookup by operation ref failed", zap.String("operation_ref", key), zap.Error(err))
}
return nil, err
}
return &result, nil
}
func (p *Payments) Upsert(ctx context.Context, record *model.PaymentRecord) error {
if record == nil {
return merrors.InvalidArgument("payment record is nil", "record")
@@ -82,6 +110,7 @@ func (p *Payments) Upsert(ctx context.Context, record *model.PaymentRecord) erro
record.OutgoingLeg = strings.TrimSpace(record.OutgoingLeg)
record.TargetChatID = strings.TrimSpace(record.TargetChatID)
record.IntentRef = strings.TrimSpace(record.IntentRef)
record.OperationRef = strings.TrimSpace(record.OperationRef)
if record.PaymentIntentID == "" {
return merrors.InvalidArgument("intention reference is required", "payment_intent_ref")
}

View File

@@ -18,6 +18,7 @@ type Repository interface {
type PaymentsStore interface {
FindByIdempotencyKey(ctx context.Context, key string) (*model.PaymentRecord, error)
FindByOperationRef(ctx context.Context, key string) (*model.PaymentRecord, error)
Upsert(ctx context.Context, record *model.PaymentRecord) error
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/gateway/tron/internal/appversion"
"github.com/tech/sendico/gateway/tron/shared"
tronstoragemodel "github.com/tech/sendico/gateway/tron/storage/model"
chainasset "github.com/tech/sendico/pkg/chain"
"github.com/tech/sendico/pkg/connector/params"
"github.com/tech/sendico/pkg/merrors"
@@ -17,6 +18,7 @@ import (
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
const chainConnectorID = "chain"
@@ -293,11 +295,21 @@ func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperatio
if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
return nil, merrors.InvalidArgument("get_operation: operation_id is required")
}
resp, err := s.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: strings.TrimSpace(req.GetOperationId())})
operationRef := strings.TrimSpace(req.GetOperationId())
if s.storage == nil || s.storage.Transfers() == nil {
return nil, merrors.Internal("get_operation: storage is not configured")
}
transfer, err := s.storage.Transfers().FindByOperationRef(ctx, "", operationRef)
if err != nil {
return nil, err
}
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(resp.GetTransfer())}, nil
if transfer == nil {
return nil, merrors.NoData("transfer not found")
}
return &connectorv1.GetOperationResponse{Operation: chainTransferToOperation(storageTransferToProto(transfer))}, nil
}
func (s *Service) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
@@ -493,6 +505,61 @@ func feeEstimateResult(resp *chainv1.EstimateTransferFeeResponse) *structpb.Stru
return result
}
func storageTransferToProto(transfer *tronstoragemodel.Transfer) *chainv1.Transfer {
if transfer == nil {
return nil
}
destination := &chainv1.TransferDestination{Memo: strings.TrimSpace(transfer.Destination.Memo)}
if managedWalletRef := strings.TrimSpace(transfer.Destination.ManagedWalletRef); managedWalletRef != "" {
destination.Destination = &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: managedWalletRef}
} else if externalAddress := strings.TrimSpace(transfer.Destination.ExternalAddress); externalAddress != "" {
destination.Destination = &chainv1.TransferDestination_ExternalAddress{ExternalAddress: externalAddress}
}
fees := make([]*chainv1.ServiceFeeBreakdown, 0, len(transfer.Fees))
for _, fee := range transfer.Fees {
fees = append(fees, &chainv1.ServiceFeeBreakdown{
FeeCode: strings.TrimSpace(fee.FeeCode),
Amount: fee.Amount,
Description: strings.TrimSpace(fee.Description),
})
}
asset := &chainv1.Asset{
Chain: shared.ChainEnumFromName(transfer.Network),
TokenSymbol: strings.TrimSpace(transfer.TokenSymbol),
ContractAddress: strings.TrimSpace(transfer.ContractAddress),
}
protoTransfer := &chainv1.Transfer{
TransferRef: strings.TrimSpace(transfer.TransferRef),
IdempotencyKey: strings.TrimSpace(transfer.IdempotencyKey),
IntentRef: strings.TrimSpace(transfer.IntentRef),
OperationRef: strings.TrimSpace(transfer.OperationRef),
OrganizationRef: strings.TrimSpace(transfer.OrganizationRef),
SourceWalletRef: strings.TrimSpace(transfer.SourceWalletRef),
Destination: destination,
Asset: asset,
RequestedAmount: shared.MonenyToProto(transfer.RequestedAmount),
NetAmount: shared.MonenyToProto(transfer.NetAmount),
Fees: fees,
Status: shared.TransferStatusToProto(transfer.Status),
TransactionHash: strings.TrimSpace(transfer.TxHash),
FailureReason: strings.TrimSpace(transfer.FailureReason),
PaymentRef: strings.TrimSpace(transfer.PaymentRef),
}
if !transfer.CreatedAt.IsZero() {
protoTransfer.CreatedAt = timestamppb.New(transfer.CreatedAt.UTC())
}
if !transfer.UpdatedAt.IsZero() {
protoTransfer.UpdatedAt = timestamppb.New(transfer.UpdatedAt.UTC())
}
return protoTransfer
}
func gasTopUpResult(amount *moneyv1.Money, capHit bool, transferRef string) *structpb.Struct {
payload := map[string]interface{}{
"cap_hit": capHit,
@@ -518,18 +585,33 @@ func chainTransferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation
return nil
}
op := &connectorv1.Operation{
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
Type: connectorv1.OperationType_TRANSFER,
Status: chainTransferStatusToOperation(transfer.GetStatus()),
Money: transfer.GetRequestedAmount(),
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
CreatedAt: transfer.GetCreatedAt(),
UpdatedAt: transfer.GetUpdatedAt(),
OperationId: strings.TrimSpace(transfer.GetTransferRef()),
Type: connectorv1.OperationType_TRANSFER,
Status: chainTransferStatusToOperation(transfer.GetStatus()),
Money: transfer.GetRequestedAmount(),
ProviderRef: strings.TrimSpace(transfer.GetTransactionHash()),
IntentRef: strings.TrimSpace(transfer.GetIntentRef()),
OperationRef: strings.TrimSpace(transfer.GetOperationRef()),
CreatedAt: transfer.GetCreatedAt(),
UpdatedAt: transfer.GetUpdatedAt(),
From: &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
ConnectorId: chainConnectorID,
AccountId: strings.TrimSpace(transfer.GetSourceWalletRef()),
}}},
}
params := map[string]interface{}{}
if paymentRef := strings.TrimSpace(transfer.GetPaymentRef()); paymentRef != "" {
params["payment_ref"] = paymentRef
}
if organizationRef := strings.TrimSpace(transfer.GetOrganizationRef()); organizationRef != "" {
params["organization_ref"] = organizationRef
}
if failureReason := strings.TrimSpace(transfer.GetFailureReason()); failureReason != "" {
params["failure_reason"] = failureReason
}
if len(params) > 0 {
op.Params = structFromMap(params)
}
if dest := transfer.GetDestination(); dest != nil {
switch d := dest.GetDestination().(type) {
case *chainv1.TransferDestination_ManagedWalletRef:
@@ -629,6 +711,17 @@ func operationAccountID(party *connectorv1.OperationParty) string {
return ""
}
func structFromMap(values map[string]interface{}) *structpb.Struct {
if len(values) == 0 {
return nil
}
result, err := structpb.NewStruct(values)
if err != nil {
return nil
}
return result
}
func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
err := &connectorv1.ConnectorError{
Code: code,

View File

@@ -554,6 +554,32 @@ func (t *inMemoryTransfers) Get(ctx context.Context, transferRef string) (*model
return transfer, nil
}
func (t *inMemoryTransfers) FindByOperationRef(ctx context.Context, organizationRef, operationRef string) (*model.Transfer, error) {
t.mu.Lock()
defer t.mu.Unlock()
org := strings.TrimSpace(organizationRef)
opRef := strings.TrimSpace(operationRef)
if opRef == "" {
return nil, merrors.InvalidArgument("transfersStore: empty operationRef")
}
for _, transfer := range t.items {
if transfer == nil {
continue
}
if !strings.EqualFold(strings.TrimSpace(transfer.OperationRef), opRef) {
continue
}
if org != "" && !strings.EqualFold(strings.TrimSpace(transfer.OrganizationRef), org) {
continue
}
return transfer, nil
}
return nil, merrors.NoData("transfer not found")
}
func (t *inMemoryTransfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) {
t.mu.Lock()
defer t.mu.Unlock()

View File

@@ -40,6 +40,9 @@ func NewTransfers(logger mlogger.Logger, db *mongo.Database) (*Transfers, error)
Keys: []ri.Key{{Field: "transferRef", Sort: ri.Asc}},
Unique: true,
},
{
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}, {Field: "operationRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}},
Unique: true,
@@ -110,6 +113,25 @@ func (t *Transfers) Get(ctx context.Context, transferRef string) (*model.Transfe
return transfer, nil
}
func (t *Transfers) FindByOperationRef(ctx context.Context, organizationRef, operationRef string) (*model.Transfer, error) {
operationRef = strings.TrimSpace(operationRef)
if operationRef == "" {
return nil, merrors.InvalidArgument("transfersStore: empty operationRef")
}
query := repository.Query().Filter(repository.Field("operationRef"), operationRef)
if org := strings.TrimSpace(organizationRef); org != "" {
query = query.Filter(repository.Field("organizationRef"), org)
}
transfer := &model.Transfer{}
if err := t.repo.FindOneByFilter(ctx, query, transfer); err != nil {
return nil, err
}
return transfer, nil
}
func (t *Transfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) {
query := repository.Query()
if src := strings.TrimSpace(filter.SourceWalletRef); src != "" {

View File

@@ -42,6 +42,7 @@ type WalletsStore interface {
type TransfersStore interface {
Create(ctx context.Context, transfer *model.Transfer) (*model.Transfer, error)
Get(ctx context.Context, transferRef string) (*model.Transfer, error)
FindByOperationRef(ctx context.Context, organizationRef, operationRef string) (*model.Transfer, error)
List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error)
UpdateStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason string, txHash string) (*model.Transfer, error)
}