616 lines
18 KiB
Go
616 lines
18 KiB
Go
package quotation
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
|
"github.com/tech/sendico/payments/storage/model"
|
|
chainpkg "github.com/tech/sendico/pkg/chain"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
|
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
|
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
|
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
|
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
|
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
type quoteRequest struct {
|
|
Meta *sharedv1.RequestMeta
|
|
IdempotencyKey string
|
|
Intent *sharedv1.PaymentIntent
|
|
}
|
|
|
|
func (r *quoteRequest) GetMeta() *sharedv1.RequestMeta {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.Meta
|
|
}
|
|
|
|
func (r *quoteRequest) GetIdempotencyKey() string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
return r.IdempotencyKey
|
|
}
|
|
|
|
func (r *quoteRequest) GetIntent() *sharedv1.PaymentIntent {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
return r.Intent
|
|
}
|
|
|
|
func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *quoteRequest) (*sharedv1.PaymentQuote, time.Time, error) {
|
|
intent := req.GetIntent()
|
|
amount := intent.GetAmount()
|
|
fxSide := fxv1.Side_SIDE_UNSPECIFIED
|
|
if fxIntent := fxIntentForQuote(intent); fxIntent != nil {
|
|
fxSide = fxIntent.GetSide()
|
|
}
|
|
|
|
var fxQuote *oraclev1.Quote
|
|
var err error
|
|
if shouldRequestFX(intent) {
|
|
fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
s.logger.Debug("Fx quote attached to payment quote", zap.String("org_ref", orgRef))
|
|
}
|
|
|
|
payAmount, settlementAmountBeforeFees := resolveTradeAmounts(amount, fxQuote, fxSide)
|
|
|
|
feeBaseAmount := payAmount
|
|
if feeBaseAmount == nil {
|
|
feeBaseAmount = cloneProtoMoney(amount)
|
|
}
|
|
|
|
intentModel := intentFromProto(intent)
|
|
sourceRail, _, err := railFromEndpoint(intentModel.Source, intentModel.Attributes, true)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
destRail, _, err := railFromEndpoint(intentModel.Destination, intentModel.Attributes, false)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
feeRequired := feesRequiredForRails(sourceRail, destRail)
|
|
feeQuote := &feesv1.PrecomputeFeesResponse{}
|
|
if feeRequired {
|
|
feeQuote, err = s.quoteFees(ctx, orgRef, req, feeBaseAmount)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
}
|
|
conversionFeeQuote := &feesv1.PrecomputeFeesResponse{}
|
|
if s.shouldQuoteConversionFee(ctx, req.GetIntent()) {
|
|
conversionFeeQuote, err = s.quoteConversionFees(ctx, orgRef, req, feeBaseAmount)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
}
|
|
feeCurrency := ""
|
|
if feeBaseAmount != nil {
|
|
feeCurrency = feeBaseAmount.GetCurrency()
|
|
} else if amount != nil {
|
|
feeCurrency = amount.GetCurrency()
|
|
}
|
|
feeLines := cloneFeeLines(feeQuote.GetLines())
|
|
if conversionFeeQuote != nil {
|
|
feeLines = append(feeLines, cloneFeeLines(conversionFeeQuote.GetLines())...)
|
|
}
|
|
s.assignFeeLedgerAccounts(intent, feeLines)
|
|
feeTotal := extractFeeTotal(feeLines, feeCurrency)
|
|
|
|
var networkFee *chainv1.EstimateTransferFeeResponse
|
|
if shouldEstimateNetworkFee(intent) {
|
|
networkFee, err = s.estimateNetworkFee(ctx, intent)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
s.logger.Debug("Network fee estimated", zap.String("org_ref", orgRef))
|
|
}
|
|
|
|
debitAmount, settlementAmount := computeAggregates(
|
|
payAmount,
|
|
settlementAmountBeforeFees,
|
|
feeTotal,
|
|
networkFee,
|
|
fxQuote,
|
|
resolvedFeeTreatmentForQuote(intent),
|
|
)
|
|
|
|
quote := &sharedv1.PaymentQuote{
|
|
DebitAmount: debitAmount,
|
|
DebitSettlementAmount: payAmount,
|
|
ExpectedSettlementAmount: settlementAmount,
|
|
ExpectedFeeTotal: feeTotal,
|
|
FeeLines: feeLines,
|
|
FeeRules: mergeFeeRules(feeQuote, conversionFeeQuote),
|
|
FxQuote: fxQuote,
|
|
NetworkFee: networkFee,
|
|
}
|
|
|
|
expiresAt := quoteExpiry(s.clock.Now(), feeQuote, fxQuote)
|
|
if conversionFeeQuote != nil {
|
|
convExpiry := quoteExpiry(s.clock.Now(), conversionFeeQuote, fxQuote)
|
|
if convExpiry.Before(expiresAt) {
|
|
expiresAt = convExpiry
|
|
}
|
|
}
|
|
|
|
return quote, expiresAt, nil
|
|
}
|
|
|
|
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *quoteRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
|
|
if !s.deps.fees.available() {
|
|
return &feesv1.PrecomputeFeesResponse{}, nil
|
|
}
|
|
intent := req.GetIntent()
|
|
amount := cloneProtoMoney(baseAmount)
|
|
if amount == nil {
|
|
amount = cloneProtoMoney(intent.GetAmount())
|
|
}
|
|
attrs := ensureFeeAttributes(intent, amount, cloneMetadata(intent.GetAttributes()))
|
|
feeIntent := &feesv1.Intent{
|
|
Trigger: feeTriggerForIntent(intent),
|
|
BaseAmount: amount,
|
|
BookedAt: timestamppb.New(s.clock.Now()),
|
|
OriginType: "payments.orchestrator.quote",
|
|
OriginRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
|
Attributes: attrs,
|
|
}
|
|
timeout := req.GetMeta().GetTrace()
|
|
ctxTimeout, cancel := s.withTimeout(ctx, s.deps.fees.timeout)
|
|
defer cancel()
|
|
resp, err := s.deps.fees.client.PrecomputeFees(ctxTimeout, &feesv1.PrecomputeFeesRequest{
|
|
Meta: &feesv1.RequestMeta{
|
|
OrganizationRef: orgRef,
|
|
Trace: timeout,
|
|
},
|
|
Intent: feeIntent,
|
|
TtlMs: defaultFeeQuoteTTLMillis,
|
|
})
|
|
if err != nil {
|
|
s.logger.Warn("Fees precompute failed", zap.Error(err))
|
|
return nil, merrors.Internal("fees_precompute_failed")
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) quoteConversionFees(ctx context.Context, orgRef string, req *quoteRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
|
|
if !s.deps.fees.available() {
|
|
return &feesv1.PrecomputeFeesResponse{}, nil
|
|
}
|
|
intent := req.GetIntent()
|
|
amount := cloneProtoMoney(baseAmount)
|
|
if amount == nil {
|
|
amount = cloneProtoMoney(intent.GetAmount())
|
|
}
|
|
attrs := ensureFeeAttributes(intent, amount, cloneMetadata(intent.GetAttributes()))
|
|
attrs["product"] = "wallet"
|
|
attrs["source_type"] = "managed_wallet"
|
|
attrs["destination_type"] = "ledger"
|
|
|
|
feeIntent := &feesv1.Intent{
|
|
Trigger: feesv1.Trigger_TRIGGER_CAPTURE,
|
|
BaseAmount: amount,
|
|
BookedAt: timestamppb.New(s.clock.Now()),
|
|
OriginType: "payments.orchestrator.conversion_quote",
|
|
OriginRef: strings.TrimSpace(req.GetIdempotencyKey()),
|
|
Attributes: attrs,
|
|
}
|
|
timeout := req.GetMeta().GetTrace()
|
|
ctxTimeout, cancel := s.withTimeout(ctx, s.deps.fees.timeout)
|
|
defer cancel()
|
|
resp, err := s.deps.fees.client.PrecomputeFees(ctxTimeout, &feesv1.PrecomputeFeesRequest{
|
|
Meta: &feesv1.RequestMeta{
|
|
OrganizationRef: orgRef,
|
|
Trace: timeout,
|
|
},
|
|
Intent: feeIntent,
|
|
TtlMs: defaultFeeQuoteTTLMillis,
|
|
})
|
|
if err != nil {
|
|
s.logger.Warn("Conversion fee precompute failed", zap.Error(err))
|
|
return nil, merrors.Internal("fees_precompute_failed")
|
|
}
|
|
setFeeLineTarget(resp.GetLines(), feeLineTargetWallet)
|
|
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
|
setFeeLineWalletRef(resp.GetLines(), src.GetManagedWalletRef(), "managed_wallet")
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) shouldQuoteConversionFee(ctx context.Context, intent *sharedv1.PaymentIntent) bool {
|
|
if intent == nil {
|
|
return false
|
|
}
|
|
if !isManagedWalletEndpoint(intent.GetSource()) {
|
|
return false
|
|
}
|
|
if isLedgerEndpoint(intent.GetDestination()) {
|
|
return false
|
|
}
|
|
if s.storage == nil {
|
|
return false
|
|
}
|
|
templates := s.storage.PlanTemplates()
|
|
if templates == nil {
|
|
return false
|
|
}
|
|
|
|
intentModel := intentFromProto(intent)
|
|
sourceRail, sourceNetwork, err := railFromEndpoint(intentModel.Source, intentModel.Attributes, true)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
destRail, destNetwork, err := railFromEndpoint(intentModel.Destination, intentModel.Attributes, false)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
network, err := resolveRouteNetwork(intentModel.Attributes, sourceNetwork, destNetwork)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
template, err := selectPlanTemplate(ctx, s.logger.Named("quote_payment"), templates, sourceRail, destRail, network)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return templateHasLedgerMove(template)
|
|
}
|
|
|
|
func templateHasLedgerMove(template *model.PaymentPlanTemplate) bool {
|
|
if template == nil {
|
|
return false
|
|
}
|
|
for _, step := range template.Steps {
|
|
if step.Rail != model.RailLedger {
|
|
continue
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(step.Operation), "ledger.move") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mergeFeeRules(primary, secondary *feesv1.PrecomputeFeesResponse) []*feesv1.AppliedRule {
|
|
rules := cloneFeeRules(nil)
|
|
if primary != nil {
|
|
rules = append(rules, cloneFeeRules(primary.GetApplied())...)
|
|
}
|
|
if secondary != nil {
|
|
rules = append(rules, cloneFeeRules(secondary.GetApplied())...)
|
|
}
|
|
if len(rules) == 0 {
|
|
return nil
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func ensureFeeAttributes(intent *sharedv1.PaymentIntent, baseAmount *moneyv1.Money, attrs map[string]string) map[string]string {
|
|
if attrs == nil {
|
|
attrs = map[string]string{}
|
|
}
|
|
if intent == nil {
|
|
return attrs
|
|
}
|
|
setFeeAttributeIfMissing(attrs, "product", "wallet")
|
|
if op := feeOperationFromKind(intent.GetKind()); op != "" {
|
|
setFeeAttributeIfMissing(attrs, "operation", op)
|
|
}
|
|
if currency := feeCurrencyFromAmount(baseAmount, intent.GetAmount()); currency != "" {
|
|
setFeeAttributeIfMissing(attrs, "currency", currency)
|
|
}
|
|
if srcType := endpointTypeFromProto(intent.GetSource()); srcType != "" {
|
|
setFeeAttributeIfMissing(attrs, "source_type", srcType)
|
|
}
|
|
if dstType := endpointTypeFromProto(intent.GetDestination()); dstType != "" {
|
|
setFeeAttributeIfMissing(attrs, "destination_type", dstType)
|
|
}
|
|
if asset := assetFromIntent(intent); asset != nil {
|
|
if token := strings.TrimSpace(asset.GetTokenSymbol()); token != "" {
|
|
setFeeAttributeIfMissing(attrs, "asset", token)
|
|
}
|
|
if chain := asset.GetChain(); chain != chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED {
|
|
if network := strings.TrimSpace(chainpkg.NetworkAlias(chain)); network != "" {
|
|
setFeeAttributeIfMissing(attrs, "network", network)
|
|
}
|
|
}
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
func feeTriggerForIntent(intent *sharedv1.PaymentIntent) feesv1.Trigger {
|
|
if intent == nil {
|
|
return feesv1.Trigger_TRIGGER_UNSPECIFIED
|
|
}
|
|
trigger := triggerFromKind(intent.GetKind(), shouldRequestFX(intent))
|
|
if trigger != feesv1.Trigger_TRIGGER_FX_CONVERSION && isManagedWalletEndpoint(intent.GetSource()) && isLedgerEndpoint(intent.GetDestination()) {
|
|
return feesv1.Trigger_TRIGGER_CAPTURE
|
|
}
|
|
return trigger
|
|
}
|
|
|
|
func isManagedWalletEndpoint(endpoint *sharedv1.PaymentEndpoint) bool {
|
|
return endpoint != nil && endpoint.GetManagedWallet() != nil
|
|
}
|
|
|
|
func isLedgerEndpoint(endpoint *sharedv1.PaymentEndpoint) bool {
|
|
return endpoint != nil && endpoint.GetLedger() != nil
|
|
}
|
|
|
|
func setFeeAttributeIfMissing(attrs map[string]string, key, value string) {
|
|
if attrs == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(key) == "" {
|
|
return
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return
|
|
}
|
|
if _, exists := attrs[key]; exists {
|
|
return
|
|
}
|
|
attrs[key] = value
|
|
}
|
|
|
|
func feeOperationFromKind(kind sharedv1.PaymentKind) string {
|
|
switch kind {
|
|
case sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT:
|
|
return "payout"
|
|
case sharedv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER:
|
|
return "internal_transfer"
|
|
case sharedv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION:
|
|
return "fx_conversion"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func feeCurrencyFromAmount(baseAmount, intentAmount *moneyv1.Money) string {
|
|
if baseAmount != nil {
|
|
if currency := strings.TrimSpace(baseAmount.GetCurrency()); currency != "" {
|
|
return currency
|
|
}
|
|
}
|
|
if intentAmount != nil {
|
|
return strings.TrimSpace(intentAmount.GetCurrency())
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func endpointTypeFromProto(endpoint *sharedv1.PaymentEndpoint) string {
|
|
if endpoint == nil {
|
|
return ""
|
|
}
|
|
switch {
|
|
case endpoint.GetLedger() != nil:
|
|
return "ledger"
|
|
case endpoint.GetManagedWallet() != nil:
|
|
return "managed_wallet"
|
|
case endpoint.GetExternalChain() != nil:
|
|
return "external_chain"
|
|
case endpoint.GetCard() != nil:
|
|
return "card"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func assetFromIntent(intent *sharedv1.PaymentIntent) *chainv1.Asset {
|
|
if intent == nil {
|
|
return nil
|
|
}
|
|
if asset := assetFromEndpoint(intent.GetDestination()); asset != nil {
|
|
return asset
|
|
}
|
|
return assetFromEndpoint(intent.GetSource())
|
|
}
|
|
|
|
func assetFromEndpoint(endpoint *sharedv1.PaymentEndpoint) *chainv1.Asset {
|
|
if endpoint == nil {
|
|
return nil
|
|
}
|
|
if wallet := endpoint.GetManagedWallet(); wallet != nil {
|
|
return wallet.GetAsset()
|
|
}
|
|
if external := endpoint.GetExternalChain(); external != nil {
|
|
return external.GetAsset()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) estimateNetworkFee(ctx context.Context, intent *sharedv1.PaymentIntent) (*chainv1.EstimateTransferFeeResponse, error) {
|
|
req := &chainv1.EstimateTransferFeeRequest{
|
|
Amount: cloneProtoMoney(intent.GetAmount()),
|
|
}
|
|
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
|
req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef())
|
|
}
|
|
if dst := intent.GetDestination().GetManagedWallet(); dst != nil {
|
|
req.Destination = &chainv1.TransferDestination{
|
|
Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())},
|
|
}
|
|
}
|
|
if dst := intent.GetDestination().GetExternalChain(); dst != nil {
|
|
req.Destination = &chainv1.TransferDestination{
|
|
Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())},
|
|
Memo: strings.TrimSpace(dst.GetMemo()),
|
|
}
|
|
req.Asset = dst.GetAsset()
|
|
}
|
|
if req.Asset == nil {
|
|
if src := intent.GetSource().GetManagedWallet(); src != nil {
|
|
req.Asset = src.GetAsset()
|
|
}
|
|
}
|
|
|
|
network := ""
|
|
if req.Asset != nil {
|
|
network = chainpkg.NetworkName(req.Asset.GetChain())
|
|
}
|
|
instanceID := strings.TrimSpace(intent.GetSource().GetInstanceId())
|
|
if instanceID == "" {
|
|
instanceID = strings.TrimSpace(intent.GetDestination().GetInstanceId())
|
|
}
|
|
client, _, err := s.resolveChainGatewayClient(ctx, network, moneyFromProto(req.Amount), []model.RailOperation{model.RailOperationSend}, instanceID, "")
|
|
if err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
s.logger.Debug("Network fee estimation skipped: gateway unavailable", zap.Error(err))
|
|
return nil, nil
|
|
}
|
|
s.logger.Warn("Chain gateway resolution failed", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
if client == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
resp, err := client.EstimateTransferFee(ctx, req)
|
|
if err != nil {
|
|
s.logger.Warn("Chain gateway fee estimation failed", zap.Error(err))
|
|
return nil, merrors.Internal("chain_gateway_fee_estimation_failed")
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *quoteRequest) (*oraclev1.Quote, error) {
|
|
intent := req.GetIntent()
|
|
fxRequired := shouldRequestFX(intent)
|
|
|
|
if !s.deps.oracle.available() {
|
|
if fxRequired {
|
|
return nil, merrors.Internal("fx_oracle_unavailable")
|
|
}
|
|
return nil, nil
|
|
}
|
|
meta := req.GetMeta()
|
|
fxIntent := fxIntentForQuote(intent)
|
|
if fxIntent == nil {
|
|
if fxRequired {
|
|
return nil, merrors.InvalidArgument("fx intent missing")
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
ttl := fxIntent.GetTtlMs()
|
|
if ttl <= 0 {
|
|
ttl = defaultOracleTTLMillis
|
|
}
|
|
|
|
params := oracleclient.GetQuoteParams{
|
|
Meta: oracleclient.RequestMeta{
|
|
OrganizationRef: orgRef,
|
|
Trace: meta.GetTrace(),
|
|
},
|
|
Pair: fxIntent.GetPair(),
|
|
Side: fxIntent.GetSide(),
|
|
Firm: fxIntent.GetFirm(),
|
|
TTL: time.Duration(ttl) * time.Millisecond,
|
|
PreferredProvider: strings.TrimSpace(fxIntent.GetPreferredProvider()),
|
|
}
|
|
|
|
if fxIntent.GetMaxAgeMs() > 0 {
|
|
params.MaxAge = time.Duration(fxIntent.GetMaxAgeMs()) * time.Millisecond
|
|
}
|
|
|
|
if amount := intent.GetAmount(); amount != nil {
|
|
pair := fxIntent.GetPair()
|
|
if pair != nil {
|
|
switch {
|
|
case strings.EqualFold(amount.GetCurrency(), pair.GetBase()):
|
|
params.BaseAmount = cloneProtoMoney(amount)
|
|
case strings.EqualFold(amount.GetCurrency(), pair.GetQuote()):
|
|
params.QuoteAmount = cloneProtoMoney(amount)
|
|
default:
|
|
params.BaseAmount = cloneProtoMoney(amount)
|
|
}
|
|
} else {
|
|
params.BaseAmount = cloneProtoMoney(amount)
|
|
}
|
|
}
|
|
|
|
quote, err := s.deps.oracle.client.GetQuote(ctx, params)
|
|
if err != nil {
|
|
s.logger.Warn("Fx oracle quote failed", zap.Error(err))
|
|
return nil, merrors.Internal(fmt.Sprintf("orchestrator: fx quote failed, %s", err.Error()))
|
|
}
|
|
if quote == nil {
|
|
if fxRequired {
|
|
return nil, merrors.Internal("orchestrator: fx quote missing")
|
|
}
|
|
return nil, nil
|
|
}
|
|
return quoteToProto(quote), nil
|
|
}
|
|
|
|
func feesRequiredForRails(sourceRail, destRail model.Rail) bool {
|
|
if sourceRail == model.RailLedger && destRail == model.RailLedger {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Service) feeLedgerAccountForIntent(intent *sharedv1.PaymentIntent) string {
|
|
if intent == nil || len(s.deps.feeLedgerAccounts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
key := s.gatewayKeyFromIntent(intent)
|
|
if key == "" {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(s.deps.feeLedgerAccounts[key])
|
|
}
|
|
|
|
func (s *Service) assignFeeLedgerAccounts(intent *sharedv1.PaymentIntent, lines []*feesv1.DerivedPostingLine) {
|
|
account := s.feeLedgerAccountForIntent(intent)
|
|
key := s.gatewayKeyFromIntent(intent)
|
|
|
|
missing := 0
|
|
for _, line := range lines {
|
|
if line == nil {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(line.GetLedgerAccountRef()) == "" {
|
|
missing++
|
|
}
|
|
}
|
|
if missing == 0 {
|
|
return
|
|
}
|
|
|
|
if account == "" {
|
|
s.logger.Debug("No fee ledger account mapping found", zap.String("gateway", key), zap.Int("missing_lines", missing))
|
|
return
|
|
}
|
|
assignLedgerAccounts(lines, account)
|
|
s.logger.Debug("Applied fee ledger account mapping", zap.String("gateway", key), zap.String("ledger_account", account), zap.Int("lines", missing))
|
|
}
|
|
|
|
func (s *Service) gatewayKeyFromIntent(intent *sharedv1.PaymentIntent) string {
|
|
if intent == nil {
|
|
return ""
|
|
}
|
|
key := strings.TrimSpace(intent.GetAttributes()["gateway"])
|
|
if key == "" {
|
|
if dest := intent.GetDestination(); dest != nil && dest.GetCard() != nil {
|
|
key = defaultCardGateway
|
|
}
|
|
}
|
|
return strings.ToLower(key)
|
|
}
|