Merge pull request 'payments-273' (#274) from payments-273 into main
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #274
This commit was merged in pull request #274.
This commit is contained in:
@@ -49,6 +49,7 @@ func (p *paymentExecutor) executePayment(ctx context.Context, store storage.Paym
|
|||||||
}
|
}
|
||||||
plan, err := builder.Build(ctx, payment, quote, routeStore, planTemplates, p.svc.deps.gatewayRegistry)
|
plan, err := builder.Build(ctx, payment, quote, routeStore, planTemplates, p.svc.deps.gatewayRegistry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
p.logPlanBuilderFailure(payment, err)
|
||||||
return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, strings.TrimSpace(err.Error()), err)
|
return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, strings.TrimSpace(err.Error()), err)
|
||||||
}
|
}
|
||||||
if plan == nil || len(plan.Steps) == 0 {
|
if plan == nil || len(plan.Steps) == 0 {
|
||||||
@@ -59,6 +60,55 @@ func (p *paymentExecutor) executePayment(ctx context.Context, store storage.Paym
|
|||||||
return p.executePaymentPlan(ctx, store, payment, quote)
|
return p.executePaymentPlan(ctx, store, payment, quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *paymentExecutor) logPlanBuilderFailure(payment *model.Payment, err error) {
|
||||||
|
if p == nil || payment == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
intent := payment.Intent
|
||||||
|
sourceRail, sourceNetwork, sourceErr := railFromEndpoint(intent.Source, intent.Attributes, true)
|
||||||
|
destRail, destNetwork, destErr := railFromEndpoint(intent.Destination, intent.Attributes, false)
|
||||||
|
|
||||||
|
fields := []zap.Field{
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("payment_ref", payment.PaymentRef),
|
||||||
|
zap.String("org_ref", payment.OrganizationRef.Hex()),
|
||||||
|
zap.String("idempotency_key", payment.IdempotencyKey),
|
||||||
|
zap.String("source_rail", string(sourceRail)),
|
||||||
|
zap.String("destination_rail", string(destRail)),
|
||||||
|
zap.String("source_network", sourceNetwork),
|
||||||
|
zap.String("destination_network", destNetwork),
|
||||||
|
zap.String("source_endpoint_type", string(intent.Source.Type)),
|
||||||
|
zap.String("destination_endpoint_type", string(intent.Destination.Type)),
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := make([]string, 0, 2)
|
||||||
|
if sourceErr != nil || sourceRail == model.RailUnspecified {
|
||||||
|
missing = append(missing, "source")
|
||||||
|
if sourceErr != nil {
|
||||||
|
fields = append(fields, zap.String("source_rail_error", sourceErr.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if destErr != nil || destRail == model.RailUnspecified {
|
||||||
|
missing = append(missing, "destination")
|
||||||
|
if destErr != nil {
|
||||||
|
fields = append(fields, zap.String("destination_rail_error", destErr.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
fields = append(fields, zap.String("missing_rails", strings.Join(missing, ",")))
|
||||||
|
p.logger.Warn("Payment rail resolution failed", fields...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
routeNetwork, routeErr := resolveRouteNetwork(intent.Attributes, sourceNetwork, destNetwork)
|
||||||
|
if routeErr != nil {
|
||||||
|
fields = append(fields, zap.String("route_network_error", routeErr.Error()))
|
||||||
|
} else if routeNetwork != "" {
|
||||||
|
fields = append(fields, zap.String("route_network", routeNetwork))
|
||||||
|
}
|
||||||
|
p.logger.Warn("Payment route missing for rails", fields...)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *paymentExecutor) applyFX(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine, description string, metadata map[string]string, exec *model.ExecutionRefs) error {
|
func (p *paymentExecutor) applyFX(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine, description string, metadata map[string]string, exec *model.ExecutionRefs) error {
|
||||||
intent := payment.Intent
|
intent := payment.Intent
|
||||||
source := intent.Source.Ledger
|
source := intent.Source.Ledger
|
||||||
|
|||||||
@@ -167,6 +167,121 @@ func TestDefaultPlanBuilder_ErrorsWhenRouteMissing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultPlanBuilder_UsesSourceCurrencyForCryptoSendWithFX(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
builder := &defaultPlanBuilder{}
|
||||||
|
|
||||||
|
payment := &model.Payment{
|
||||||
|
PaymentRef: "pay-2",
|
||||||
|
IdempotencyKey: "idem-2",
|
||||||
|
Intent: model.PaymentIntent{
|
||||||
|
Kind: model.PaymentKindPayout,
|
||||||
|
RequiresFX: true,
|
||||||
|
Source: model.PaymentEndpoint{
|
||||||
|
Type: model.EndpointTypeManagedWallet,
|
||||||
|
ManagedWallet: &model.ManagedWalletEndpoint{
|
||||||
|
ManagedWalletRef: "wallet-2",
|
||||||
|
Asset: &paymenttypes.Asset{
|
||||||
|
Chain: "TRON",
|
||||||
|
TokenSymbol: "USDT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Destination: model.PaymentEndpoint{
|
||||||
|
Type: model.EndpointTypeCard,
|
||||||
|
Card: &model.CardEndpoint{MaskedPan: "4111"},
|
||||||
|
},
|
||||||
|
Amount: &paymenttypes.Money{Currency: "USDT", Amount: "1.4"},
|
||||||
|
SettlementMode: model.SettlementModeFixReceived,
|
||||||
|
SettlementCurrency: "RUB",
|
||||||
|
},
|
||||||
|
LastQuote: &model.PaymentQuoteSnapshot{
|
||||||
|
DebitAmount: &paymenttypes.Money{Currency: "USDT", Amount: "1.498"},
|
||||||
|
ExpectedSettlementAmount: &paymenttypes.Money{Currency: "RUB", Amount: "108.99"},
|
||||||
|
ExpectedFeeTotal: &paymenttypes.Money{Currency: "USDT", Amount: "0.098"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
quote := &orchestratorv1.PaymentQuote{
|
||||||
|
DebitAmount: &moneyv1.Money{Currency: "USDT", Amount: "1.498"},
|
||||||
|
ExpectedSettlementAmount: &moneyv1.Money{Currency: "RUB", Amount: "108.99"},
|
||||||
|
ExpectedFeeTotal: &moneyv1.Money{Currency: "USDT", Amount: "0.098"},
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := &stubRouteStore{
|
||||||
|
routes: []*model.PaymentRoute{
|
||||||
|
{FromRail: model.RailCrypto, ToRail: model.RailCardPayout, Network: "TRON", IsEnabled: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
templates := &stubPlanTemplateStore{
|
||||||
|
templates: []*model.PaymentPlanTemplate{
|
||||||
|
{
|
||||||
|
FromRail: model.RailCrypto,
|
||||||
|
ToRail: model.RailCardPayout,
|
||||||
|
Network: "TRON",
|
||||||
|
IsEnabled: true,
|
||||||
|
Steps: []model.OrchestrationStep{
|
||||||
|
{StepID: "crypto_send", Rail: model.RailCrypto, Operation: "payout.crypto"},
|
||||||
|
{StepID: "crypto_fee", Rail: model.RailCrypto, Operation: "fee.send", DependsOn: []string{"crypto_send"}},
|
||||||
|
{StepID: "crypto_observe", Rail: model.RailCrypto, Operation: "observe.confirm", DependsOn: []string{"crypto_send"}},
|
||||||
|
{StepID: "ledger_credit", Rail: model.RailLedger, Operation: "ledger.credit", DependsOn: []string{"crypto_observe"}},
|
||||||
|
{StepID: "card_payout", Rail: model.RailCardPayout, Operation: "payout.card", DependsOn: []string{"ledger_credit"}},
|
||||||
|
{StepID: "ledger_debit", Rail: model.RailLedger, Operation: "ledger.debit", DependsOn: []string{"card_payout"}, CommitPolicy: model.CommitPolicyAfterSuccess, CommitAfter: []string{"card_payout"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := &stubGatewayRegistry{
|
||||||
|
items: []*model.GatewayInstanceDescriptor{
|
||||||
|
{
|
||||||
|
ID: "crypto-tron",
|
||||||
|
InstanceID: "crypto-tron-2",
|
||||||
|
Rail: model.RailCrypto,
|
||||||
|
Network: "TRON",
|
||||||
|
Currencies: []string{"USDT"},
|
||||||
|
Capabilities: model.RailCapabilities{
|
||||||
|
CanPayOut: true,
|
||||||
|
CanSendFee: true,
|
||||||
|
RequiresObserveConfirm: true,
|
||||||
|
},
|
||||||
|
Limits: model.Limits{MinAmount: "0", MaxAmount: "100000"},
|
||||||
|
IsEnabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "card",
|
||||||
|
InstanceID: "card-2",
|
||||||
|
Rail: model.RailCardPayout,
|
||||||
|
Currencies: []string{"RUB"},
|
||||||
|
Capabilities: model.RailCapabilities{
|
||||||
|
CanPayOut: true,
|
||||||
|
},
|
||||||
|
Limits: model.Limits{MinAmount: "0", MaxAmount: "100000"},
|
||||||
|
IsEnabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := builder.Build(ctx, payment, quote, routes, templates, registry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected plan, got error: %v", err)
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
t.Fatal("expected plan")
|
||||||
|
}
|
||||||
|
if len(plan.Steps) != 6 {
|
||||||
|
t.Fatalf("expected 6 steps, got %d", len(plan.Steps))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertPlanStep(t, plan.Steps[0], "crypto_send", model.RailCrypto, model.RailOperationSend, "crypto-tron", "crypto-tron-2", "USDT", "1.4")
|
||||||
|
assertPlanStep(t, plan.Steps[1], "crypto_fee", model.RailCrypto, model.RailOperationFee, "crypto-tron", "crypto-tron-2", "USDT", "0.098")
|
||||||
|
assertPlanStep(t, plan.Steps[2], "crypto_observe", model.RailCrypto, model.RailOperationObserveConfirm, "crypto-tron", "crypto-tron-2", "", "")
|
||||||
|
assertPlanStep(t, plan.Steps[3], "ledger_credit", model.RailLedger, model.RailOperationCredit, "", "", "RUB", "108.99")
|
||||||
|
assertPlanStep(t, plan.Steps[4], "card_payout", model.RailCardPayout, model.RailOperationSend, "card", "card-2", "RUB", "108.99")
|
||||||
|
assertPlanStep(t, plan.Steps[5], "ledger_debit", model.RailLedger, model.RailOperationDebit, "", "", "RUB", "108.99")
|
||||||
|
}
|
||||||
|
|
||||||
// --- test doubles ---
|
// --- test doubles ---
|
||||||
|
|
||||||
type stubRouteStore struct {
|
type stubRouteStore struct {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func resolveSettlementAmount(payment *model.Payment, quote *orchestratorv1.Payme
|
|||||||
if quote != nil && quote.GetExpectedSettlementAmount() != nil {
|
if quote != nil && quote.GetExpectedSettlementAmount() != nil {
|
||||||
return moneyFromProto(quote.GetExpectedSettlementAmount())
|
return moneyFromProto(quote.GetExpectedSettlementAmount())
|
||||||
}
|
}
|
||||||
if payment != nil && payment.LastQuote != nil {
|
if payment != nil && payment.LastQuote != nil && payment.LastQuote.ExpectedSettlementAmount != nil {
|
||||||
return cloneMoney(payment.LastQuote.ExpectedSettlementAmount)
|
return cloneMoney(payment.LastQuote.ExpectedSettlementAmount)
|
||||||
}
|
}
|
||||||
return cloneMoney(fallback)
|
return cloneMoney(fallback)
|
||||||
@@ -73,7 +73,7 @@ func resolveDebitAmount(payment *model.Payment, quote *orchestratorv1.PaymentQuo
|
|||||||
if quote != nil && quote.GetDebitAmount() != nil {
|
if quote != nil && quote.GetDebitAmount() != nil {
|
||||||
return moneyFromProto(quote.GetDebitAmount())
|
return moneyFromProto(quote.GetDebitAmount())
|
||||||
}
|
}
|
||||||
if payment != nil && payment.LastQuote != nil {
|
if payment != nil && payment.LastQuote != nil && payment.LastQuote.DebitAmount != nil {
|
||||||
return cloneMoney(payment.LastQuote.DebitAmount)
|
return cloneMoney(payment.LastQuote.DebitAmount)
|
||||||
}
|
}
|
||||||
return cloneMoney(fallback)
|
return cloneMoney(fallback)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
||||||
"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"
|
||||||
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ func (b *defaultPlanBuilder) buildPlanFromTemplate(ctx context.Context, payment
|
|||||||
}
|
}
|
||||||
feeAmount := resolveFeeAmount(payment, quote)
|
feeAmount := resolveFeeAmount(payment, quote)
|
||||||
feeRequired := isPositiveMoney(feeAmount)
|
feeRequired := isPositiveMoney(feeAmount)
|
||||||
|
sourceSendAmount, err := netSourceAmount(sourceAmount, feeAmount, quote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
payoutAmount := settlementAmount
|
payoutAmount := settlementAmount
|
||||||
if destRail == model.RailCardPayout {
|
if destRail == model.RailCardPayout {
|
||||||
@@ -63,7 +68,7 @@ func (b *defaultPlanBuilder) buildPlanFromTemplate(ctx context.Context, payment
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
amount, err := stepAmountForAction(action, tpl.Rail, sourceRail, destRail, sourceAmount, settlementAmount, payoutAmount, feeAmount, ledgerDebitAmount, ledgerCreditAmount, feeRequired)
|
amount, err := stepAmountForAction(action, tpl.Rail, sourceRail, destRail, sourceSendAmount, settlementAmount, payoutAmount, feeAmount, ledgerDebitAmount, ledgerCreditAmount, feeRequired)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -94,7 +99,7 @@ func (b *defaultPlanBuilder) buildPlanFromTemplate(ctx context.Context, payment
|
|||||||
instanceID := stepInstanceIDForRail(payment.Intent, tpl.Rail, sourceRail, destRail)
|
instanceID := stepInstanceIDForRail(payment.Intent, tpl.Rail, sourceRail, destRail)
|
||||||
checkAmount := amount
|
checkAmount := amount
|
||||||
if action == model.RailOperationObserveConfirm {
|
if action == model.RailOperationObserveConfirm {
|
||||||
checkAmount = observeAmountForRail(tpl.Rail, sourceAmount, settlementAmount, payoutAmount)
|
checkAmount = observeAmountForRail(tpl.Rail, sourceSendAmount, settlementAmount, payoutAmount)
|
||||||
}
|
}
|
||||||
gw, err := ensureGatewayForAction(ctx, gateways, gatewaysByRail, tpl.Rail, network, checkAmount, action, instanceID, sendDirectionForRail(tpl.Rail))
|
gw, err := ensureGatewayForAction(ctx, gateways, gatewaysByRail, tpl.Rail, network, checkAmount, action, instanceID, sendDirectionForRail(tpl.Rail))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -165,7 +170,7 @@ func actionForOperation(operation string) (model.RailOperation, error) {
|
|||||||
return model.RailOperationUnspecified, merrors.InvalidArgument("plan builder: unsupported operation")
|
return model.RailOperationUnspecified, merrors.InvalidArgument("plan builder: unsupported operation")
|
||||||
}
|
}
|
||||||
|
|
||||||
func stepAmountForAction(action model.RailOperation, rail, sourceRail, destRail model.Rail, sourceAmount, settlementAmount, payoutAmount, feeAmount, ledgerDebitAmount, ledgerCreditAmount *paymenttypes.Money, feeRequired bool) (*paymenttypes.Money, error) {
|
func stepAmountForAction(action model.RailOperation, rail, sourceRail, destRail model.Rail, sourceSendAmount, settlementAmount, payoutAmount, feeAmount, ledgerDebitAmount, ledgerCreditAmount *paymenttypes.Money, feeRequired bool) (*paymenttypes.Money, error) {
|
||||||
switch action {
|
switch action {
|
||||||
case model.RailOperationDebit:
|
case model.RailOperationDebit:
|
||||||
if rail == model.RailLedger {
|
if rail == model.RailLedger {
|
||||||
@@ -180,10 +185,7 @@ func stepAmountForAction(action model.RailOperation, rail, sourceRail, destRail
|
|||||||
case model.RailOperationSend:
|
case model.RailOperationSend:
|
||||||
switch rail {
|
switch rail {
|
||||||
case sourceRail:
|
case sourceRail:
|
||||||
if feeRequired {
|
return cloneMoney(sourceSendAmount), nil
|
||||||
return cloneMoney(settlementAmount), nil
|
|
||||||
}
|
|
||||||
return cloneMoney(sourceAmount), nil
|
|
||||||
case destRail:
|
case destRail:
|
||||||
return cloneMoney(payoutAmount), nil
|
return cloneMoney(payoutAmount), nil
|
||||||
default:
|
default:
|
||||||
@@ -221,12 +223,12 @@ func stepInstanceIDForRail(intent model.PaymentIntent, rail, sourceRail, destRai
|
|||||||
func observeAmountForRail(rail model.Rail, source, settlement, payout *paymenttypes.Money) *paymenttypes.Money {
|
func observeAmountForRail(rail model.Rail, source, settlement, payout *paymenttypes.Money) *paymenttypes.Money {
|
||||||
switch rail {
|
switch rail {
|
||||||
case model.RailCrypto, model.RailFiatOnRamp:
|
case model.RailCrypto, model.RailFiatOnRamp:
|
||||||
if settlement != nil {
|
|
||||||
return settlement
|
|
||||||
}
|
|
||||||
if source != nil {
|
if source != nil {
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
if settlement != nil {
|
||||||
|
return settlement
|
||||||
|
}
|
||||||
case model.RailProviderSettlement:
|
case model.RailProviderSettlement:
|
||||||
if settlement != nil {
|
if settlement != nil {
|
||||||
return settlement
|
return settlement
|
||||||
@@ -242,6 +244,48 @@ func observeAmountForRail(rail model.Rail, source, settlement, payout *paymentty
|
|||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func netSourceAmount(sourceAmount, feeAmount *paymenttypes.Money, quote *orchestratorv1.PaymentQuote) (*paymenttypes.Money, error) {
|
||||||
|
if sourceAmount == nil {
|
||||||
|
return nil, merrors.InvalidArgument("plan builder: source amount is required")
|
||||||
|
}
|
||||||
|
netAmount := cloneMoney(sourceAmount)
|
||||||
|
if !isPositiveMoney(feeAmount) {
|
||||||
|
return netAmount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currency := strings.TrimSpace(sourceAmount.GetCurrency())
|
||||||
|
if currency == "" {
|
||||||
|
return netAmount, nil
|
||||||
|
}
|
||||||
|
var fxQuote *oraclev1.Quote
|
||||||
|
if quote != nil {
|
||||||
|
fxQuote = quote.GetFxQuote()
|
||||||
|
}
|
||||||
|
convertedFee, err := ensureCurrency(protoMoney(feeAmount), currency, fxQuote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if convertedFee == nil {
|
||||||
|
return netAmount, nil
|
||||||
|
}
|
||||||
|
sourceValue, err := decimalFromMoney(sourceAmount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
feeValue, err := decimalFromMoney(convertedFee)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
netValue := sourceValue.Sub(feeValue)
|
||||||
|
if netValue.IsNegative() {
|
||||||
|
return nil, merrors.InvalidArgument("plan builder: fee exceeds source amount")
|
||||||
|
}
|
||||||
|
return &paymenttypes.Money{
|
||||||
|
Currency: currency,
|
||||||
|
Amount: netValue.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func requireMoney(amount *paymenttypes.Money, label string) (*paymenttypes.Money, error) {
|
func requireMoney(amount *paymenttypes.Money, label string) (*paymenttypes.Money, error) {
|
||||||
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
if amount == nil || strings.TrimSpace(amount.GetAmount()) == "" || strings.TrimSpace(amount.GetCurrency()) == "" {
|
||||||
return nil, merrors.InvalidArgument("plan builder: " + label + " is required")
|
return nil, merrors.InvalidArgument("plan builder: " + label + " is required")
|
||||||
|
|||||||
Reference in New Issue
Block a user