- legacy payment template fee lines picking
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
package quotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/payments/quotation/internal/service/plan"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
)
|
||||
|
||||
func railFromEndpoint(endpoint model.PaymentEndpoint, attrs map[string]string, isSource bool) (model.Rail, string, error) {
|
||||
@@ -15,14 +12,3 @@ func railFromEndpoint(endpoint model.PaymentEndpoint, attrs map[string]string, i
|
||||
func resolveRouteNetwork(attrs map[string]string, sourceNetwork, destNetwork string) (string, error) {
|
||||
return plan.ResolveRouteNetwork(attrs, sourceNetwork, destNetwork)
|
||||
}
|
||||
|
||||
func selectPlanTemplate(
|
||||
ctx context.Context,
|
||||
logger mlogger.Logger,
|
||||
templates plan.PlanTemplateStore,
|
||||
sourceRail model.Rail,
|
||||
destRail model.Rail,
|
||||
network string,
|
||||
) (*model.PaymentPlanTemplate, error) {
|
||||
return plan.SelectTemplate(ctx, logger, templates, sourceRail, destRail, network)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *quo
|
||||
}
|
||||
}
|
||||
conversionFeeQuote := &feesv1.PrecomputeFeesResponse{}
|
||||
if s.shouldQuoteConversionFee(ctx, req.GetIntent()) {
|
||||
if s.shouldQuoteConversionFee(req.GetIntent()) {
|
||||
conversionFeeQuote, err = s.quoteConversionFees(ctx, orgRef, req, feeBaseAmount)
|
||||
if err != nil {
|
||||
return nil, time.Time{}, err
|
||||
@@ -230,7 +230,7 @@ func (s *Service) quoteConversionFees(ctx context.Context, orgRef string, req *q
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) shouldQuoteConversionFee(ctx context.Context, intent *sharedv1.PaymentIntent) bool {
|
||||
func (s *Service) shouldQuoteConversionFee(intent *sharedv1.PaymentIntent) bool {
|
||||
if intent == nil {
|
||||
return false
|
||||
}
|
||||
@@ -240,48 +240,7 @@ func (s *Service) shouldQuoteConversionFee(ctx context.Context, intent *sharedv1
|
||||
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
|
||||
return true
|
||||
}
|
||||
|
||||
func mergeFeeRules(primary, secondary *feesv1.PrecomputeFeesResponse) []*feesv1.AppliedRule {
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package quotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestBuildPaymentQuote_RequestsConversionFeesWithoutPlanTemplates(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
|
||||
"payments.orchestrator.quote": {
|
||||
Lines: []*feesv1.DerivedPostingLine{
|
||||
testFeeLine("1.00", "USDT"),
|
||||
},
|
||||
Applied: []*feesv1.AppliedRule{{RuleId: "rule.base"}},
|
||||
},
|
||||
"payments.orchestrator.conversion_quote": {
|
||||
Lines: []*feesv1.DerivedPostingLine{
|
||||
testFeeLine("2.00", "USDT"),
|
||||
},
|
||||
Applied: []*feesv1.AppliedRule{{RuleId: "rule.conversion"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToCardIntent(),
|
||||
}
|
||||
|
||||
quote, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err != nil {
|
||||
t.Fatalf("buildPaymentQuote returned error: %v", err)
|
||||
}
|
||||
if quote == nil {
|
||||
t.Fatalf("expected quote")
|
||||
}
|
||||
|
||||
if got, want := len(feeClient.precomputeReqs), 2; got != want {
|
||||
t.Fatalf("unexpected precompute call count: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.quote"), 1; got != want {
|
||||
t.Fatalf("unexpected base precompute calls: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.conversion_quote"), 1; got != want {
|
||||
t.Fatalf("unexpected conversion precompute calls: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := len(quote.GetFeeLines()), 2; got != want {
|
||||
t.Fatalf("unexpected fee lines count: got=%d want=%d", got, want)
|
||||
}
|
||||
if quote.GetExpectedFeeTotal() == nil {
|
||||
t.Fatalf("expected fee total")
|
||||
}
|
||||
if got, want := quote.GetExpectedFeeTotal().GetCurrency(), "USDT"; got != want {
|
||||
t.Fatalf("unexpected fee total currency: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := quote.GetExpectedFeeTotal().GetAmount(), "3"; got != want {
|
||||
t.Fatalf("unexpected fee total amount: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := quote.GetFeeLines()[1].GetMeta()[feeLineMetaTarget]; got != feeLineTargetWallet {
|
||||
t.Fatalf("expected conversion line target %q, got %q", feeLineTargetWallet, got)
|
||||
}
|
||||
if got, want := quote.GetFeeLines()[1].GetMeta()[feeLineMetaWalletRef], "mw_src_1"; got != want {
|
||||
t.Fatalf("unexpected conversion fee wallet_ref: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := len(quote.GetFeeRules()), 2; got != want {
|
||||
t.Fatalf("unexpected fee rules count: got=%d want=%d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ConversionFeeReturnedEmptyDoesNotAddLines(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
|
||||
"payments.orchestrator.quote": {
|
||||
Lines: []*feesv1.DerivedPostingLine{
|
||||
testFeeLine("1.00", "USDT"),
|
||||
},
|
||||
},
|
||||
"payments.orchestrator.conversion_quote": {},
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToCardIntent(),
|
||||
}
|
||||
|
||||
quote, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err != nil {
|
||||
t.Fatalf("buildPaymentQuote returned error: %v", err)
|
||||
}
|
||||
if quote == nil {
|
||||
t.Fatalf("expected quote")
|
||||
}
|
||||
|
||||
if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.conversion_quote"), 1; got != want {
|
||||
t.Fatalf("unexpected conversion precompute calls: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := len(quote.GetFeeLines()), 1; got != want {
|
||||
t.Fatalf("unexpected fee lines count: got=%d want=%d", got, want)
|
||||
}
|
||||
if quote.GetFeeLines()[0].GetMeta()[feeLineMetaTarget] == feeLineTargetWallet {
|
||||
t.Fatalf("base fee line should not be tagged as wallet conversion line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_DoesNotRequestConversionFeesForManagedWalletToLedger(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
|
||||
"payments.orchestrator.quote": {
|
||||
Lines: []*feesv1.DerivedPostingLine{
|
||||
testFeeLine("1.00", "USDT"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToLedgerIntent(),
|
||||
}
|
||||
|
||||
quote, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err != nil {
|
||||
t.Fatalf("buildPaymentQuote returned error: %v", err)
|
||||
}
|
||||
if quote == nil {
|
||||
t.Fatalf("expected quote")
|
||||
}
|
||||
|
||||
if got, want := len(feeClient.precomputeReqs), 1; got != want {
|
||||
t.Fatalf("unexpected precompute call count: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := precomputeCallsByOrigin(feeClient.precomputeReqs, "payments.orchestrator.conversion_quote"), 0; got != want {
|
||||
t.Fatalf("unexpected conversion precompute calls: got=%d want=%d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
type stubFeeEngineClient struct {
|
||||
precomputeByOrigin map[string]*feesv1.PrecomputeFeesResponse
|
||||
precomputeReqs []*feesv1.PrecomputeFeesRequest
|
||||
}
|
||||
|
||||
func (s *stubFeeEngineClient) QuoteFees(context.Context, *feesv1.QuoteFeesRequest, ...grpc.CallOption) (*feesv1.QuoteFeesResponse, error) {
|
||||
return nil, merrors.InvalidArgument("unexpected QuoteFees call")
|
||||
}
|
||||
|
||||
func (s *stubFeeEngineClient) PrecomputeFees(_ context.Context, in *feesv1.PrecomputeFeesRequest, _ ...grpc.CallOption) (*feesv1.PrecomputeFeesResponse, error) {
|
||||
if s == nil {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
}
|
||||
if in != nil {
|
||||
if cloned, ok := proto.Clone(in).(*feesv1.PrecomputeFeesRequest); ok {
|
||||
s.precomputeReqs = append(s.precomputeReqs, cloned)
|
||||
} else {
|
||||
s.precomputeReqs = append(s.precomputeReqs, in)
|
||||
}
|
||||
}
|
||||
if in == nil || in.GetIntent() == nil {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
}
|
||||
|
||||
originType := strings.TrimSpace(in.GetIntent().GetOriginType())
|
||||
resp, ok := s.precomputeByOrigin[originType]
|
||||
if !ok || resp == nil {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
}
|
||||
if cloned, ok := proto.Clone(resp).(*feesv1.PrecomputeFeesResponse); ok {
|
||||
return cloned, nil
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *stubFeeEngineClient) ValidateFeeToken(context.Context, *feesv1.ValidateFeeTokenRequest, ...grpc.CallOption) (*feesv1.ValidateFeeTokenResponse, error) {
|
||||
return nil, merrors.InvalidArgument("unexpected ValidateFeeToken call")
|
||||
}
|
||||
|
||||
func precomputeCallsByOrigin(reqs []*feesv1.PrecomputeFeesRequest, originType string) int {
|
||||
count := 0
|
||||
for _, req := range reqs {
|
||||
if req == nil || req.GetIntent() == nil {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(req.GetIntent().GetOriginType()), strings.TrimSpace(originType)) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func testFeeLine(amount, currency string) *feesv1.DerivedPostingLine {
|
||||
return &feesv1.DerivedPostingLine{
|
||||
Money: &moneyv1.Money{
|
||||
Amount: amount,
|
||||
Currency: currency,
|
||||
},
|
||||
LineType: accountingv1.PostingLineType_POSTING_LINE_FEE,
|
||||
Side: accountingv1.EntrySide_ENTRY_SIDE_DEBIT,
|
||||
}
|
||||
}
|
||||
|
||||
func testManagedWalletToCardIntent() *sharedv1.PaymentIntent {
|
||||
return &sharedv1.PaymentIntent{
|
||||
Kind: sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT,
|
||||
Source: &sharedv1.PaymentEndpoint{
|
||||
Endpoint: &sharedv1.PaymentEndpoint_ManagedWallet{
|
||||
ManagedWallet: &sharedv1.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "mw_src_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: &sharedv1.PaymentEndpoint{
|
||||
Endpoint: &sharedv1.PaymentEndpoint_Card{
|
||||
Card: &sharedv1.CardEndpoint{
|
||||
Card: &sharedv1.CardEndpoint_Pan{Pan: "4111111111111111"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Amount: &moneyv1.Money{
|
||||
Amount: "100",
|
||||
Currency: "USDT",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testManagedWalletToLedgerIntent() *sharedv1.PaymentIntent {
|
||||
return &sharedv1.PaymentIntent{
|
||||
Kind: sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT,
|
||||
Source: &sharedv1.PaymentEndpoint{
|
||||
Endpoint: &sharedv1.PaymentEndpoint_ManagedWallet{
|
||||
ManagedWallet: &sharedv1.ManagedWalletEndpoint{
|
||||
ManagedWalletRef: "mw_src_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
Destination: &sharedv1.PaymentEndpoint{
|
||||
Endpoint: &sharedv1.PaymentEndpoint_Ledger{
|
||||
Ledger: &sharedv1.LedgerEndpoint{
|
||||
LedgerAccountRef: "ledger_dst_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
Amount: &moneyv1.Money{
|
||||
Amount: "100",
|
||||
Currency: "USDT",
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user