gas tanking before transaction
This commit is contained in:
@@ -59,6 +59,7 @@ type clientConfig struct {
|
||||
type cardGatewayRouteConfig struct {
|
||||
FundingAddress string `yaml:"funding_address"`
|
||||
FeeAddress string `yaml:"fee_address"`
|
||||
FeeWalletRef string `yaml:"fee_wallet_ref"`
|
||||
}
|
||||
|
||||
func (c clientConfig) address() string {
|
||||
@@ -323,6 +324,7 @@ func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]or
|
||||
result[trimmedKey] = orchestrator.CardGatewayRoute{
|
||||
FundingAddress: strings.TrimSpace(route.FundingAddress),
|
||||
FeeAddress: strings.TrimSpace(route.FeeAddress),
|
||||
FeeWalletRef: strings.TrimSpace(route.FeeWalletRef),
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -7,13 +7,21 @@ import (
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const defaultCardGateway = "monetix"
|
||||
const (
|
||||
defaultCardGateway = "monetix"
|
||||
|
||||
stepCodeGasTopUp = "gas_top_up"
|
||||
stepCodeFundingTransfer = "funding_transfer"
|
||||
stepCodeCardPayout = "card_payout"
|
||||
stepCodeFeeTransfer = "fee_transfer"
|
||||
)
|
||||
|
||||
func (s *Service) cardRoute(gateway string) (CardGatewayRoute, error) {
|
||||
if len(s.deps.cardRoutes) == 0 {
|
||||
@@ -54,24 +62,204 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sourceWalletRef := strings.TrimSpace(source.ManagedWalletRef)
|
||||
fundingAddress := strings.TrimSpace(route.FundingAddress)
|
||||
feeWalletRef := strings.TrimSpace(route.FeeWalletRef)
|
||||
|
||||
amount := cloneMoney(intent.Amount)
|
||||
if amount == nil {
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card funding: amount is required")
|
||||
}
|
||||
|
||||
payoutAmount, err := cardPayoutAmount(payment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
feeMoney := (*moneyv1.Money)(nil)
|
||||
if quote != nil {
|
||||
feeMoney = quote.GetExpectedFeeTotal()
|
||||
}
|
||||
if feeMoney == nil && payment.LastQuote != nil {
|
||||
feeMoney = payment.LastQuote.ExpectedFeeTotal
|
||||
}
|
||||
feeDecimal := decimal.Zero
|
||||
if feeMoney != nil && strings.TrimSpace(feeMoney.GetAmount()) != "" {
|
||||
if strings.TrimSpace(feeMoney.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card funding: fee currency is required")
|
||||
}
|
||||
feeDecimal, err = decimalFromMoney(feeMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
feeRequired := feeDecimal.IsPositive()
|
||||
|
||||
fundingDest := &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: fundingAddress},
|
||||
}
|
||||
fundingFee, err := s.estimateTransferNetworkFee(ctx, sourceWalletRef, fundingDest, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var feeTransferFee *moneyv1.Money
|
||||
if feeRequired {
|
||||
if feeWalletRef == "" {
|
||||
return merrors.InvalidArgument("card funding: fee wallet ref is required when fee exists")
|
||||
}
|
||||
feeDest := &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: feeWalletRef},
|
||||
}
|
||||
feeTransferFee, err = s.estimateTransferNetworkFee(ctx, sourceWalletRef, feeDest, feeMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
requiredGas, gasCurrency, err := sumNetworkFees(fundingFee, feeTransferFee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
balanceResp, err := s.deps.gateway.client.GetWalletBalance(ctx, &chainv1.GetWalletBalanceRequest{
|
||||
WalletRef: sourceWalletRef,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("card funding balance check failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
|
||||
return err
|
||||
}
|
||||
if balanceResp == nil {
|
||||
return merrors.Internal("card funding: balance unavailable")
|
||||
}
|
||||
var nativeAvailable *moneyv1.Money
|
||||
if balance := balanceResp.GetBalance(); balance != nil {
|
||||
nativeAvailable = balance.GetNativeAvailable()
|
||||
}
|
||||
available := decimal.Zero
|
||||
availableCurrency := ""
|
||||
if nativeAvailable != nil && strings.TrimSpace(nativeAvailable.GetAmount()) != "" {
|
||||
if strings.TrimSpace(nativeAvailable.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card funding: native balance currency is required")
|
||||
}
|
||||
available, err = decimalFromMoney(nativeAvailable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
availableCurrency = strings.TrimSpace(nativeAvailable.GetCurrency())
|
||||
}
|
||||
if requiredGas.IsPositive() {
|
||||
if availableCurrency == "" {
|
||||
availableCurrency = gasCurrency
|
||||
}
|
||||
if gasCurrency != "" && availableCurrency != "" && !strings.EqualFold(gasCurrency, availableCurrency) {
|
||||
return merrors.InvalidArgument("card funding: native balance currency mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
topUpAmount := decimal.Zero
|
||||
if requiredGas.IsPositive() {
|
||||
topUpAmount = requiredGas.Sub(available)
|
||||
if topUpAmount.IsNegative() {
|
||||
topUpAmount = decimal.Zero
|
||||
}
|
||||
}
|
||||
|
||||
var topUpMoney *moneyv1.Money
|
||||
var topUpFee *moneyv1.Money
|
||||
if topUpAmount.IsPositive() {
|
||||
if feeWalletRef == "" {
|
||||
return merrors.InvalidArgument("card funding: fee wallet ref is required for gas top-up")
|
||||
}
|
||||
if gasCurrency == "" {
|
||||
gasCurrency = availableCurrency
|
||||
}
|
||||
if gasCurrency == "" {
|
||||
return merrors.InvalidArgument("card funding: native currency is required for gas top-up")
|
||||
}
|
||||
topUpMoney = makeMoney(gasCurrency, topUpAmount)
|
||||
topUpDest := &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
|
||||
}
|
||||
topUpFee, err = s.estimateTransferNetworkFee(ctx, feeWalletRef, topUpDest, topUpMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
plan := ensureExecutionPlan(payment)
|
||||
var gasStep *model.ExecutionStep
|
||||
if topUpMoney != nil && topUpAmount.IsPositive() {
|
||||
gasStep = ensureExecutionStep(plan, stepCodeGasTopUp)
|
||||
gasStep.Description = "Top up native gas from fee wallet"
|
||||
gasStep.Amount = cloneMoney(topUpMoney)
|
||||
gasStep.NetworkFee = cloneMoney(topUpFee)
|
||||
gasStep.SourceWalletRef = feeWalletRef
|
||||
gasStep.DestinationRef = sourceWalletRef
|
||||
}
|
||||
|
||||
fundStep := ensureExecutionStep(plan, stepCodeFundingTransfer)
|
||||
fundStep.Description = "Transfer payout amount to card funding wallet"
|
||||
fundStep.Amount = cloneMoney(amount)
|
||||
fundStep.NetworkFee = cloneMoney(fundingFee)
|
||||
fundStep.SourceWalletRef = sourceWalletRef
|
||||
fundStep.DestinationRef = fundingAddress
|
||||
|
||||
cardStep := ensureExecutionStep(plan, stepCodeCardPayout)
|
||||
cardStep.Description = "Submit card payout"
|
||||
cardStep.Amount = cloneMoney(payoutAmount)
|
||||
if card := intent.Destination.Card; card != nil {
|
||||
if masked := strings.TrimSpace(card.MaskedPan); masked != "" {
|
||||
cardStep.DestinationRef = masked
|
||||
}
|
||||
}
|
||||
|
||||
if feeRequired {
|
||||
step := ensureExecutionStep(plan, stepCodeFeeTransfer)
|
||||
step.Description = "Transfer fee to fee wallet"
|
||||
step.Amount = cloneMoney(feeMoney)
|
||||
step.NetworkFee = cloneMoney(feeTransferFee)
|
||||
step.SourceWalletRef = sourceWalletRef
|
||||
step.DestinationRef = feeWalletRef
|
||||
}
|
||||
|
||||
updateExecutionPlanTotalNetworkFee(plan)
|
||||
|
||||
exec := payment.Execution
|
||||
if exec == nil {
|
||||
exec = &model.ExecutionRefs{}
|
||||
}
|
||||
|
||||
if topUpMoney != nil && topUpAmount.IsPositive() {
|
||||
gasReq := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: payment.IdempotencyKey + ":card:gas",
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
SourceWalletRef: feeWalletRef,
|
||||
Destination: &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: sourceWalletRef},
|
||||
},
|
||||
Amount: topUpMoney,
|
||||
Metadata: cloneMetadata(payment.Metadata),
|
||||
ClientReference: payment.PaymentRef,
|
||||
}
|
||||
gasResp, gasErr := s.deps.gateway.client.SubmitTransfer(ctx, gasReq)
|
||||
if gasErr != nil {
|
||||
s.logger.Warn("card gas top-up transfer failed", zap.Error(gasErr), zap.String("payment_ref", payment.PaymentRef))
|
||||
return gasErr
|
||||
}
|
||||
if gasResp != nil && gasResp.GetTransfer() != nil {
|
||||
gasStep.TransferRef = strings.TrimSpace(gasResp.GetTransfer().GetTransferRef())
|
||||
}
|
||||
s.logger.Info("card gas top-up transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", gasStep.TransferRef))
|
||||
}
|
||||
|
||||
// Transfer payout amount to funding wallet.
|
||||
fundReq := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: payment.IdempotencyKey + ":card:fund",
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
|
||||
SourceWalletRef: sourceWalletRef,
|
||||
Destination: &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(route.FundingAddress)},
|
||||
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: fundingAddress},
|
||||
},
|
||||
Amount: amount,
|
||||
Metadata: cloneMetadata(payment.Metadata),
|
||||
@@ -84,42 +272,10 @@ func (s *Service) submitCardFundingTransfers(ctx context.Context, payment *model
|
||||
}
|
||||
if fundResp != nil && fundResp.GetTransfer() != nil {
|
||||
exec.ChainTransferRef = strings.TrimSpace(fundResp.GetTransfer().GetTransferRef())
|
||||
fundStep.TransferRef = exec.ChainTransferRef
|
||||
}
|
||||
s.logger.Info("card funding transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", exec.ChainTransferRef))
|
||||
|
||||
feeMoney := quote.GetExpectedFeeTotal()
|
||||
if feeMoney != nil && strings.TrimSpace(feeMoney.GetAmount()) != "" {
|
||||
if strings.TrimSpace(route.FeeAddress) == "" {
|
||||
return merrors.InvalidArgument("card funding: fee address is required when fee exists")
|
||||
}
|
||||
feeDecimal, err := decimalFromMoney(feeMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if feeDecimal.IsPositive() {
|
||||
feeReq := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: payment.IdempotencyKey + ":card:fee",
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef),
|
||||
Destination: &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(route.FeeAddress)},
|
||||
},
|
||||
Amount: feeMoney,
|
||||
Metadata: cloneMetadata(payment.Metadata),
|
||||
ClientReference: payment.PaymentRef,
|
||||
}
|
||||
feeResp, feeErr := s.deps.gateway.client.SubmitTransfer(ctx, feeReq)
|
||||
if feeErr != nil {
|
||||
s.logger.Warn("card fee transfer failed", zap.Error(feeErr), zap.String("payment_ref", payment.PaymentRef))
|
||||
return feeErr
|
||||
}
|
||||
if feeResp != nil && feeResp.GetTransfer() != nil {
|
||||
exec.FeeTransferRef = strings.TrimSpace(feeResp.GetTransfer().GetTransferRef())
|
||||
}
|
||||
s.logger.Info("card fee transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", exec.FeeTransferRef))
|
||||
}
|
||||
}
|
||||
|
||||
payment.Execution = exec
|
||||
return nil
|
||||
}
|
||||
@@ -133,9 +289,9 @@ func (s *Service) submitCardPayout(ctx context.Context, payment *model.Payment)
|
||||
if card == nil {
|
||||
return merrors.InvalidArgument("card payout: card endpoint is required")
|
||||
}
|
||||
amount := cloneMoney(intent.Amount)
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card payout: amount is required")
|
||||
amount, err := cardPayoutAmount(payment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
amtDec, err := decimalFromMoney(amount)
|
||||
if err != nil {
|
||||
@@ -193,13 +349,92 @@ func (s *Service) submitCardPayout(ctx context.Context, payment *model.Payment)
|
||||
return merrors.Internal("card payout: missing payout state")
|
||||
}
|
||||
recordCardPayoutState(payment, state)
|
||||
if payment.Execution == nil {
|
||||
payment.Execution = &model.ExecutionRefs{}
|
||||
exec := payment.Execution
|
||||
if exec == nil {
|
||||
exec = &model.ExecutionRefs{}
|
||||
}
|
||||
if payment.Execution.CardPayoutRef == "" {
|
||||
payment.Execution.CardPayoutRef = strings.TrimSpace(state.GetPayoutId())
|
||||
if exec.CardPayoutRef == "" {
|
||||
exec.CardPayoutRef = strings.TrimSpace(state.GetPayoutId())
|
||||
}
|
||||
s.logger.Info("card payout submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("payout_id", payment.Execution.CardPayoutRef))
|
||||
payment.Execution = exec
|
||||
|
||||
plan := ensureExecutionPlan(payment)
|
||||
if plan != nil {
|
||||
step := ensureExecutionStep(plan, stepCodeCardPayout)
|
||||
step.Description = "Submit card payout"
|
||||
step.Amount = cloneMoney(amount)
|
||||
if masked := strings.TrimSpace(card.MaskedPan); masked != "" {
|
||||
step.DestinationRef = masked
|
||||
}
|
||||
if exec.CardPayoutRef != "" {
|
||||
step.TransferRef = exec.CardPayoutRef
|
||||
}
|
||||
updateExecutionPlanTotalNetworkFee(plan)
|
||||
}
|
||||
|
||||
feeMoney := (*moneyv1.Money)(nil)
|
||||
if payment.LastQuote != nil {
|
||||
feeMoney = payment.LastQuote.ExpectedFeeTotal
|
||||
}
|
||||
if feeMoney != nil && strings.TrimSpace(feeMoney.GetAmount()) != "" {
|
||||
if strings.TrimSpace(feeMoney.GetCurrency()) == "" {
|
||||
return merrors.InvalidArgument("card payout: fee currency is required")
|
||||
}
|
||||
feeDecimal, err := decimalFromMoney(feeMoney)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if feeDecimal.IsPositive() {
|
||||
if !s.deps.gateway.available() {
|
||||
s.logger.Warn("card fee aborted: chain gateway unavailable")
|
||||
return merrors.InvalidArgument("card payout: chain gateway unavailable")
|
||||
}
|
||||
sourceWallet := intent.Source.ManagedWallet
|
||||
if sourceWallet == nil || strings.TrimSpace(sourceWallet.ManagedWalletRef) == "" {
|
||||
return merrors.InvalidArgument("card payout: source managed wallet is required")
|
||||
}
|
||||
route, err := s.cardRoute(defaultCardGateway)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
feeWalletRef := strings.TrimSpace(route.FeeWalletRef)
|
||||
if feeWalletRef == "" {
|
||||
return merrors.InvalidArgument("card payout: fee wallet ref is required when fee exists")
|
||||
}
|
||||
feeReq := &chainv1.SubmitTransferRequest{
|
||||
IdempotencyKey: payment.IdempotencyKey + ":card:fee",
|
||||
OrganizationRef: payment.OrganizationRef.Hex(),
|
||||
SourceWalletRef: strings.TrimSpace(sourceWallet.ManagedWalletRef),
|
||||
Destination: &chainv1.TransferDestination{
|
||||
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: feeWalletRef},
|
||||
},
|
||||
Amount: feeMoney,
|
||||
Metadata: cloneMetadata(payment.Metadata),
|
||||
ClientReference: payment.PaymentRef,
|
||||
}
|
||||
feeResp, feeErr := s.deps.gateway.client.SubmitTransfer(ctx, feeReq)
|
||||
if feeErr != nil {
|
||||
s.logger.Warn("card fee transfer failed", zap.Error(feeErr), zap.String("payment_ref", payment.PaymentRef))
|
||||
return feeErr
|
||||
}
|
||||
if feeResp != nil && feeResp.GetTransfer() != nil {
|
||||
exec.FeeTransferRef = strings.TrimSpace(feeResp.GetTransfer().GetTransferRef())
|
||||
}
|
||||
s.logger.Info("card fee transfer submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", exec.FeeTransferRef))
|
||||
|
||||
if plan != nil {
|
||||
step := ensureExecutionStep(plan, stepCodeFeeTransfer)
|
||||
step.Description = "Transfer fee to fee wallet"
|
||||
step.Amount = cloneMoney(feeMoney)
|
||||
step.SourceWalletRef = strings.TrimSpace(sourceWallet.ManagedWalletRef)
|
||||
step.DestinationRef = feeWalletRef
|
||||
step.TransferRef = exec.FeeTransferRef
|
||||
updateExecutionPlanTotalNetworkFee(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("card payout submitted", zap.String("payment_ref", payment.PaymentRef), zap.String("payout_id", exec.CardPayoutRef))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -250,3 +485,147 @@ func applyCardPayoutUpdate(payment *model.Payment, payout *mntxv1.CardPayoutStat
|
||||
// leave as-is for pending/unspecified
|
||||
}
|
||||
}
|
||||
|
||||
func cardPayoutAmount(payment *model.Payment) (*moneyv1.Money, error) {
|
||||
if payment == nil {
|
||||
return nil, merrors.InvalidArgument("payment is required")
|
||||
}
|
||||
amount := cloneMoney(payment.Intent.Amount)
|
||||
if payment.LastQuote != nil {
|
||||
settlement := payment.LastQuote.ExpectedSettlementAmount
|
||||
if settlement != nil && strings.TrimSpace(settlement.GetAmount()) != "" && strings.TrimSpace(settlement.GetCurrency()) != "" {
|
||||
amount = cloneMoney(settlement)
|
||||
}
|
||||
}
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
return nil, merrors.InvalidArgument("card payout: amount is required")
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func (s *Service) estimateTransferNetworkFee(ctx context.Context, sourceWalletRef string, destination *chainv1.TransferDestination, amount *moneyv1.Money) (*moneyv1.Money, error) {
|
||||
if !s.deps.gateway.available() {
|
||||
return nil, merrors.InvalidArgument("chain gateway unavailable")
|
||||
}
|
||||
sourceWalletRef = strings.TrimSpace(sourceWalletRef)
|
||||
if sourceWalletRef == "" {
|
||||
return nil, merrors.InvalidArgument("source wallet ref is required")
|
||||
}
|
||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||
return nil, merrors.InvalidArgument("amount is required")
|
||||
}
|
||||
|
||||
resp, err := s.deps.gateway.client.EstimateTransferFee(ctx, &chainv1.EstimateTransferFeeRequest{
|
||||
SourceWalletRef: sourceWalletRef,
|
||||
Destination: destination,
|
||||
Amount: cloneMoney(amount),
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("chain gateway fee estimation failed", zap.Error(err), zap.String("source_wallet_ref", sourceWalletRef))
|
||||
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
|
||||
}
|
||||
if resp == nil {
|
||||
s.logger.Warn("chain gateway fee estimation returned empty response", zap.String("source_wallet_ref", sourceWalletRef))
|
||||
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
|
||||
}
|
||||
fee := resp.GetNetworkFee()
|
||||
if fee == nil || strings.TrimSpace(fee.GetAmount()) == "" || strings.TrimSpace(fee.GetCurrency()) == "" {
|
||||
s.logger.Warn("chain gateway fee estimation missing network fee", zap.String("source_wallet_ref", sourceWalletRef))
|
||||
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
|
||||
}
|
||||
return cloneMoney(fee), nil
|
||||
}
|
||||
|
||||
func sumNetworkFees(fees ...*moneyv1.Money) (decimal.Decimal, string, error) {
|
||||
total := decimal.Zero
|
||||
currency := ""
|
||||
for _, fee := range fees {
|
||||
if fee == nil {
|
||||
continue
|
||||
}
|
||||
amount := strings.TrimSpace(fee.GetAmount())
|
||||
feeCurrency := strings.TrimSpace(fee.GetCurrency())
|
||||
if amount == "" || feeCurrency == "" {
|
||||
return decimal.Zero, "", merrors.InvalidArgument("network fee is required")
|
||||
}
|
||||
value, err := decimalFromMoney(fee)
|
||||
if err != nil {
|
||||
return decimal.Zero, "", err
|
||||
}
|
||||
if currency == "" {
|
||||
currency = feeCurrency
|
||||
} else if !strings.EqualFold(currency, feeCurrency) {
|
||||
return decimal.Zero, "", merrors.InvalidArgument("network fee currency mismatch")
|
||||
}
|
||||
total = total.Add(value)
|
||||
}
|
||||
return total, currency, nil
|
||||
}
|
||||
|
||||
func ensureExecutionPlan(payment *model.Payment) *model.ExecutionPlan {
|
||||
if payment == nil {
|
||||
return nil
|
||||
}
|
||||
if payment.ExecutionPlan == nil {
|
||||
payment.ExecutionPlan = &model.ExecutionPlan{}
|
||||
}
|
||||
return payment.ExecutionPlan
|
||||
}
|
||||
|
||||
func ensureExecutionStep(plan *model.ExecutionPlan, code string) *model.ExecutionStep {
|
||||
if plan == nil {
|
||||
return nil
|
||||
}
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return nil
|
||||
}
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(step.Code, code) {
|
||||
if step.Code == "" {
|
||||
step.Code = code
|
||||
}
|
||||
return step
|
||||
}
|
||||
}
|
||||
step := &model.ExecutionStep{Code: code}
|
||||
plan.Steps = append(plan.Steps, step)
|
||||
return step
|
||||
}
|
||||
|
||||
func updateExecutionPlanTotalNetworkFee(plan *model.ExecutionPlan) {
|
||||
if plan == nil {
|
||||
return
|
||||
}
|
||||
total := decimal.Zero
|
||||
currency := ""
|
||||
hasFee := false
|
||||
for _, step := range plan.Steps {
|
||||
if step == nil || step.NetworkFee == nil {
|
||||
continue
|
||||
}
|
||||
fee := step.NetworkFee
|
||||
if strings.TrimSpace(fee.GetAmount()) == "" || strings.TrimSpace(fee.GetCurrency()) == "" {
|
||||
continue
|
||||
}
|
||||
if currency == "" {
|
||||
currency = strings.TrimSpace(fee.GetCurrency())
|
||||
} else if !strings.EqualFold(currency, fee.GetCurrency()) {
|
||||
continue
|
||||
}
|
||||
value, err := decimalFromMoney(fee)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
total = total.Add(value)
|
||||
hasFee = true
|
||||
}
|
||||
if !hasFee || currency == "" {
|
||||
plan.TotalNetworkFee = nil
|
||||
return
|
||||
}
|
||||
plan.TotalNetworkFee = makeMoney(currency, total)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||
mo "github.com/tech/sendico/pkg/model"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
||||
mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
|
||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestSubmitCardFundingTransfers_PlansTopUpAndFunding(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
const (
|
||||
sourceWalletRef = "wallet-src"
|
||||
feeWalletRef = "wallet-fee"
|
||||
fundingAddress = "0xfunding"
|
||||
)
|
||||
|
||||
var estimateCalls []*chainv1.EstimateTransferFeeRequest
|
||||
var submitCalls []*chainv1.SubmitTransferRequest
|
||||
|
||||
gateway := &chainclient.Fake{
|
||||
EstimateTransferFeeFn: func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||
estimateCalls = append(estimateCalls, req)
|
||||
dest := req.GetDestination()
|
||||
if req.GetSourceWalletRef() == feeWalletRef {
|
||||
return &chainv1.EstimateTransferFeeResponse{
|
||||
NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.005"},
|
||||
}, nil
|
||||
}
|
||||
if dest != nil && strings.TrimSpace(dest.GetExternalAddress()) != "" {
|
||||
return &chainv1.EstimateTransferFeeResponse{
|
||||
NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.01"},
|
||||
}, nil
|
||||
}
|
||||
return &chainv1.EstimateTransferFeeResponse{
|
||||
NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.02"},
|
||||
}, nil
|
||||
},
|
||||
GetWalletBalanceFn: func(ctx context.Context, req *chainv1.GetWalletBalanceRequest) (*chainv1.GetWalletBalanceResponse, error) {
|
||||
return &chainv1.GetWalletBalanceResponse{
|
||||
Balance: &chainv1.WalletBalance{
|
||||
NativeAvailable: &moneyv1.Money{Currency: "ETH", Amount: "0.005"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
submitCalls = append(submitCalls, req)
|
||||
return &chainv1.SubmitTransferResponse{
|
||||
Transfer: &chainv1.Transfer{TransferRef: req.GetIdempotencyKey()},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := &Service{
|
||||
logger: zap.NewNop(),
|
||||
deps: serviceDependencies{
|
||||
gateway: gatewayDependency{client: gateway},
|
||||
cardRoutes: map[string]CardGatewayRoute{
|
||||
defaultCardGateway: {
|
||||
FundingAddress: fundingAddress,
|
||||
FeeWalletRef: feeWalletRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
payment := &model.Payment{
|
||||
PaymentRef: "pay-1",
|
||||
IdempotencyKey: "pay-1",
|
||||
OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: primitive.NewObjectID()},
|
||||
Intent: model.PaymentIntent{
|
||||
Kind: model.PaymentKindPayout,
|
||||
Source: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeManagedWallet,
|
||||
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: sourceWalletRef,
|
||||
},
|
||||
},
|
||||
Destination: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{
|
||||
MaskedPan: "4111",
|
||||
},
|
||||
},
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
||||
},
|
||||
}
|
||||
|
||||
quote := &orchestratorv1.PaymentQuote{
|
||||
ExpectedFeeTotal: &moneyv1.Money{Currency: "USDT", Amount: "0.35"},
|
||||
}
|
||||
|
||||
if err := svc.submitCardFundingTransfers(ctx, payment, quote); err != nil {
|
||||
t.Fatalf("submitCardFundingTransfers error: %v", err)
|
||||
}
|
||||
|
||||
if len(estimateCalls) != 3 {
|
||||
t.Fatalf("expected 3 fee estimates, got %d", len(estimateCalls))
|
||||
}
|
||||
if len(submitCalls) != 2 {
|
||||
t.Fatalf("expected 2 transfer submissions, got %d", len(submitCalls))
|
||||
}
|
||||
|
||||
gasCall := findSubmitCall(t, submitCalls, "pay-1:card:gas")
|
||||
if gasCall.GetSourceWalletRef() != feeWalletRef {
|
||||
t.Fatalf("gas top-up source wallet mismatch: %s", gasCall.GetSourceWalletRef())
|
||||
}
|
||||
if gasCall.GetDestination().GetManagedWalletRef() != sourceWalletRef {
|
||||
t.Fatalf("gas top-up destination mismatch: %s", gasCall.GetDestination().GetManagedWalletRef())
|
||||
}
|
||||
if gasCall.GetAmount().GetCurrency() != "ETH" || gasCall.GetAmount().GetAmount() != "0.025" {
|
||||
t.Fatalf("gas top-up amount mismatch: %s %s", gasCall.GetAmount().GetCurrency(), gasCall.GetAmount().GetAmount())
|
||||
}
|
||||
|
||||
fundCall := findSubmitCall(t, submitCalls, "pay-1:card:fund")
|
||||
if fundCall.GetDestination().GetExternalAddress() != fundingAddress {
|
||||
t.Fatalf("funding destination mismatch: %s", fundCall.GetDestination().GetExternalAddress())
|
||||
}
|
||||
if fundCall.GetAmount().GetCurrency() != "USDT" || fundCall.GetAmount().GetAmount() != "5" {
|
||||
t.Fatalf("funding amount mismatch: %s %s", fundCall.GetAmount().GetCurrency(), fundCall.GetAmount().GetAmount())
|
||||
}
|
||||
|
||||
if payment.Execution == nil || payment.Execution.ChainTransferRef != "pay-1:card:fund" {
|
||||
t.Fatalf("expected funding transfer ref recorded, got %v", payment.Execution)
|
||||
}
|
||||
|
||||
plan := payment.ExecutionPlan
|
||||
if plan == nil {
|
||||
t.Fatal("expected execution plan to be populated")
|
||||
}
|
||||
gasStep := findExecutionStep(t, plan, stepCodeGasTopUp)
|
||||
if gasStep.Amount.GetAmount() != "0.025" || gasStep.Amount.GetCurrency() != "ETH" {
|
||||
t.Fatalf("gas step amount mismatch: %s %s", gasStep.Amount.GetCurrency(), gasStep.Amount.GetAmount())
|
||||
}
|
||||
if gasStep.NetworkFee.GetAmount() != "0.005" || gasStep.NetworkFee.GetCurrency() != "ETH" {
|
||||
t.Fatalf("gas step fee mismatch: %s %s", gasStep.NetworkFee.GetCurrency(), gasStep.NetworkFee.GetAmount())
|
||||
}
|
||||
if gasStep.TransferRef == "" {
|
||||
t.Fatalf("expected gas step transfer ref to be set")
|
||||
}
|
||||
|
||||
fundStep := findExecutionStep(t, plan, stepCodeFundingTransfer)
|
||||
if fundStep.NetworkFee.GetAmount() != "0.01" || fundStep.NetworkFee.GetCurrency() != "ETH" {
|
||||
t.Fatalf("funding step fee mismatch: %s %s", fundStep.NetworkFee.GetCurrency(), fundStep.NetworkFee.GetAmount())
|
||||
}
|
||||
if fundStep.TransferRef != "pay-1:card:fund" {
|
||||
t.Fatalf("funding step transfer ref mismatch: %s", fundStep.TransferRef)
|
||||
}
|
||||
|
||||
cardStep := findExecutionStep(t, plan, stepCodeCardPayout)
|
||||
if cardStep.Amount.GetAmount() != "5" || cardStep.Amount.GetCurrency() != "USDT" {
|
||||
t.Fatalf("card step amount mismatch: %s %s", cardStep.Amount.GetCurrency(), cardStep.Amount.GetAmount())
|
||||
}
|
||||
|
||||
feeStep := findExecutionStep(t, plan, stepCodeFeeTransfer)
|
||||
if feeStep.Amount.GetAmount() != "0.35" || feeStep.Amount.GetCurrency() != "USDT" {
|
||||
t.Fatalf("fee step amount mismatch: %s %s", feeStep.Amount.GetCurrency(), feeStep.Amount.GetAmount())
|
||||
}
|
||||
if feeStep.NetworkFee.GetAmount() != "0.02" || feeStep.NetworkFee.GetCurrency() != "ETH" {
|
||||
t.Fatalf("fee step network fee mismatch: %s %s", feeStep.NetworkFee.GetCurrency(), feeStep.NetworkFee.GetAmount())
|
||||
}
|
||||
if feeStep.TransferRef != "" {
|
||||
t.Fatalf("expected fee step transfer ref to be empty before payout, got %s", feeStep.TransferRef)
|
||||
}
|
||||
|
||||
if plan.TotalNetworkFee == nil || plan.TotalNetworkFee.GetAmount() != "0.035" || plan.TotalNetworkFee.GetCurrency() != "ETH" {
|
||||
t.Fatalf("total network fee mismatch: %v", plan.TotalNetworkFee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitCardPayout_UsesSettlementAmountAndTransfersFee(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
const (
|
||||
sourceWalletRef = "wallet-src"
|
||||
feeWalletRef = "wallet-fee"
|
||||
)
|
||||
|
||||
var payoutReq *mntxv1.CardPayoutRequest
|
||||
var submitCalls []*chainv1.SubmitTransferRequest
|
||||
|
||||
gateway := &chainclient.Fake{
|
||||
SubmitTransferFn: func(ctx context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) {
|
||||
submitCalls = append(submitCalls, req)
|
||||
return &chainv1.SubmitTransferResponse{
|
||||
Transfer: &chainv1.Transfer{TransferRef: "fee-transfer"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
mntx := &mntxclient.Fake{
|
||||
CreateCardPayoutFn: func(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
|
||||
payoutReq = req
|
||||
return &mntxv1.CardPayoutResponse{
|
||||
Payout: &mntxv1.CardPayoutState{
|
||||
PayoutId: "payout-1",
|
||||
Status: mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := &Service{
|
||||
logger: zap.NewNop(),
|
||||
deps: serviceDependencies{
|
||||
gateway: gatewayDependency{client: gateway},
|
||||
mntx: mntxDependency{client: mntx},
|
||||
cardRoutes: map[string]CardGatewayRoute{
|
||||
defaultCardGateway: {
|
||||
FundingAddress: "0xfunding",
|
||||
FeeWalletRef: feeWalletRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
payment := &model.Payment{
|
||||
PaymentRef: "pay-2",
|
||||
IdempotencyKey: "pay-2",
|
||||
OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: primitive.NewObjectID()},
|
||||
Intent: model.PaymentIntent{
|
||||
Kind: model.PaymentKindPayout,
|
||||
Source: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeManagedWallet,
|
||||
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: sourceWalletRef,
|
||||
},
|
||||
},
|
||||
Destination: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{
|
||||
Pan: "5536913762657597",
|
||||
Cardholder: "Stephan",
|
||||
},
|
||||
},
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
||||
},
|
||||
LastQuote: &model.PaymentQuoteSnapshot{
|
||||
ExpectedSettlementAmount: &moneyv1.Money{Currency: "RUB", Amount: "392.30"},
|
||||
ExpectedFeeTotal: &moneyv1.Money{Currency: "USDT", Amount: "0.35"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := svc.submitCardPayout(ctx, payment); err != nil {
|
||||
t.Fatalf("submitCardPayout error: %v", err)
|
||||
}
|
||||
|
||||
if payoutReq == nil {
|
||||
t.Fatal("expected card payout request to be sent")
|
||||
}
|
||||
if payoutReq.GetCurrency() != "RUB" || payoutReq.GetAmountMinor() != 39230 {
|
||||
t.Fatalf("payout request amount mismatch: %s %d", payoutReq.GetCurrency(), payoutReq.GetAmountMinor())
|
||||
}
|
||||
|
||||
if payment.Execution == nil || payment.Execution.CardPayoutRef != "payout-1" {
|
||||
t.Fatalf("expected card payout ref recorded, got %v", payment.Execution)
|
||||
}
|
||||
if payment.Execution.FeeTransferRef != "fee-transfer" {
|
||||
t.Fatalf("expected fee transfer ref recorded, got %v", payment.Execution)
|
||||
}
|
||||
|
||||
if len(submitCalls) != 1 {
|
||||
t.Fatalf("expected 1 fee transfer submission, got %d", len(submitCalls))
|
||||
}
|
||||
feeCall := submitCalls[0]
|
||||
if feeCall.GetSourceWalletRef() != sourceWalletRef {
|
||||
t.Fatalf("fee transfer source mismatch: %s", feeCall.GetSourceWalletRef())
|
||||
}
|
||||
if feeCall.GetDestination().GetManagedWalletRef() != feeWalletRef {
|
||||
t.Fatalf("fee transfer destination mismatch: %s", feeCall.GetDestination().GetManagedWalletRef())
|
||||
}
|
||||
|
||||
plan := payment.ExecutionPlan
|
||||
if plan == nil {
|
||||
t.Fatal("expected execution plan to be populated")
|
||||
}
|
||||
cardStep := findExecutionStep(t, plan, stepCodeCardPayout)
|
||||
if cardStep.TransferRef != "payout-1" {
|
||||
t.Fatalf("card step transfer ref mismatch: %s", cardStep.TransferRef)
|
||||
}
|
||||
if cardStep.Amount.GetAmount() != "392.30" || cardStep.Amount.GetCurrency() != "RUB" {
|
||||
t.Fatalf("card step amount mismatch: %s %s", cardStep.Amount.GetCurrency(), cardStep.Amount.GetAmount())
|
||||
}
|
||||
|
||||
feeStep := findExecutionStep(t, plan, stepCodeFeeTransfer)
|
||||
if feeStep.TransferRef != "fee-transfer" {
|
||||
t.Fatalf("fee step transfer ref mismatch: %s", feeStep.TransferRef)
|
||||
}
|
||||
if feeStep.Amount.GetAmount() != "0.35" || feeStep.Amount.GetCurrency() != "USDT" {
|
||||
t.Fatalf("fee step amount mismatch: %s %s", feeStep.Amount.GetCurrency(), feeStep.Amount.GetAmount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitCardFundingTransfers_RequiresFeeWalletRef(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
gateway := &chainclient.Fake{
|
||||
EstimateTransferFeeFn: func(ctx context.Context, req *chainv1.EstimateTransferFeeRequest) (*chainv1.EstimateTransferFeeResponse, error) {
|
||||
return &chainv1.EstimateTransferFeeResponse{
|
||||
NetworkFee: &moneyv1.Money{Currency: "ETH", Amount: "0.01"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := &Service{
|
||||
logger: zap.NewNop(),
|
||||
deps: serviceDependencies{
|
||||
gateway: gatewayDependency{client: gateway},
|
||||
cardRoutes: map[string]CardGatewayRoute{
|
||||
defaultCardGateway: {
|
||||
FundingAddress: "0xfunding",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
payment := &model.Payment{
|
||||
PaymentRef: "pay-3",
|
||||
IdempotencyKey: "pay-3",
|
||||
OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: primitive.NewObjectID()},
|
||||
Intent: model.PaymentIntent{
|
||||
Kind: model.PaymentKindPayout,
|
||||
Source: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeManagedWallet,
|
||||
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "wallet-src",
|
||||
},
|
||||
},
|
||||
Destination: model.PaymentEndpoint{
|
||||
Type: model.EndpointTypeCard,
|
||||
Card: &model.CardEndpoint{
|
||||
MaskedPan: "4111",
|
||||
},
|
||||
},
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "5"},
|
||||
},
|
||||
}
|
||||
|
||||
quote := &orchestratorv1.PaymentQuote{
|
||||
ExpectedFeeTotal: &moneyv1.Money{Currency: "USDT", Amount: "0.35"},
|
||||
}
|
||||
|
||||
err := svc.submitCardFundingTransfers(ctx, payment, quote)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing fee wallet ref")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fee wallet ref") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func findSubmitCall(t *testing.T, calls []*chainv1.SubmitTransferRequest, idempotencyKey string) *chainv1.SubmitTransferRequest {
|
||||
t.Helper()
|
||||
for _, call := range calls {
|
||||
if call.GetIdempotencyKey() == idempotencyKey {
|
||||
return call
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing submit transfer call for %s", idempotencyKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func findExecutionStep(t *testing.T, plan *model.ExecutionPlan, code string) *model.ExecutionStep {
|
||||
t.Helper()
|
||||
if plan == nil {
|
||||
t.Fatal("execution plan is nil")
|
||||
}
|
||||
for _, step := range plan.Steps {
|
||||
if step != nil && strings.EqualFold(step.Code, code) {
|
||||
return step
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing execution step %s", code)
|
||||
return nil
|
||||
}
|
||||
@@ -125,6 +125,7 @@ func toProtoPayment(src *model.Payment) *orchestratorv1.Payment {
|
||||
FailureReason: src.FailureReason,
|
||||
LastQuote: modelQuoteToProto(src.LastQuote),
|
||||
Execution: protoExecutionFromModel(src.Execution),
|
||||
ExecutionPlan: protoExecutionPlanFromModel(src.ExecutionPlan),
|
||||
Metadata: cloneMetadata(src.Metadata),
|
||||
}
|
||||
if src.CardPayout != nil {
|
||||
@@ -251,6 +252,41 @@ func protoExecutionFromModel(src *model.ExecutionRefs) *orchestratorv1.Execution
|
||||
}
|
||||
}
|
||||
|
||||
func protoExecutionStepFromModel(src *model.ExecutionStep) *orchestratorv1.ExecutionStep {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
return &orchestratorv1.ExecutionStep{
|
||||
Code: src.Code,
|
||||
Description: src.Description,
|
||||
Amount: cloneMoney(src.Amount),
|
||||
NetworkFee: cloneMoney(src.NetworkFee),
|
||||
SourceWalletRef: src.SourceWalletRef,
|
||||
DestinationRef: src.DestinationRef,
|
||||
TransferRef: src.TransferRef,
|
||||
Metadata: cloneMetadata(src.Metadata),
|
||||
}
|
||||
}
|
||||
|
||||
func protoExecutionPlanFromModel(src *model.ExecutionPlan) *orchestratorv1.ExecutionPlan {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
steps := make([]*orchestratorv1.ExecutionStep, 0, len(src.Steps))
|
||||
for _, step := range src.Steps {
|
||||
if protoStep := protoExecutionStepFromModel(step); protoStep != nil {
|
||||
steps = append(steps, protoStep)
|
||||
}
|
||||
}
|
||||
if len(steps) == 0 {
|
||||
steps = nil
|
||||
}
|
||||
return &orchestratorv1.ExecutionPlan{
|
||||
Steps: steps,
|
||||
TotalNetworkFee: cloneMoney(src.TotalNetworkFee),
|
||||
}
|
||||
}
|
||||
|
||||
func modelQuoteToProto(src *model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQuote {
|
||||
if src == nil {
|
||||
return nil
|
||||
|
||||
@@ -56,10 +56,11 @@ func (m mntxDependency) available() bool {
|
||||
return m.client != nil
|
||||
}
|
||||
|
||||
// CardGatewayRoute maps a gateway to its funding and fee destinations (addresses).
|
||||
// CardGatewayRoute maps a gateway to its funding and fee destinations.
|
||||
type CardGatewayRoute struct {
|
||||
FundingAddress string
|
||||
FeeAddress string
|
||||
FeeWalletRef string
|
||||
}
|
||||
|
||||
// WithFeeEngine wires the fee engine client.
|
||||
|
||||
Reference in New Issue
Block a user