Fixed billing fees unreachable error propagation. Added USDT ledger creation. Fixed ledger boundaries operation types
This commit is contained in:
@@ -53,8 +53,8 @@ func (i *Imp) startDiscoveryAnnouncer(cfg *config, producer msg.Producer) {
|
||||
announce := discovery.Announcement{
|
||||
Service: "PAYMENTS_METHODS",
|
||||
Operations: []string{
|
||||
"payment_methods.manage",
|
||||
"payment_methods.read",
|
||||
discovery.OperationPaymentMethodsManage,
|
||||
discovery.OperationPaymentMethodsRead,
|
||||
},
|
||||
InvokeURI: invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
package serverimp
|
||||
|
||||
import (
|
||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
)
|
||||
|
||||
type orchestratorDeps struct {
|
||||
feesClient feesv1.FeeEngineClient
|
||||
ledgerClient ledgerclient.Client
|
||||
mntxClient mntxclient.Client
|
||||
oracleClient oracleclient.Client
|
||||
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
|
||||
}
|
||||
|
||||
@@ -26,9 +22,7 @@ func (i *Imp) initDependencies(_ *config) *orchestratorDeps {
|
||||
}
|
||||
|
||||
i.discoveryClients = newDiscoveryClientResolver(i.logger, i.discoveryReg)
|
||||
deps.feesClient = &discoveryFeeClient{resolver: i.discoveryClients}
|
||||
deps.ledgerClient = &discoveryLedgerClient{resolver: i.discoveryClients}
|
||||
deps.oracleClient = &discoveryOracleClient{resolver: i.discoveryClients}
|
||||
deps.mntxClient = &discoveryMntxClient{resolver: i.discoveryClients}
|
||||
deps.gatewayInvokeResolver = discoveryGatewayInvokeResolver{resolver: i.discoveryClients}
|
||||
return deps
|
||||
@@ -39,9 +33,6 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
return nil
|
||||
}
|
||||
opts := []orchestrator.Option{}
|
||||
if deps.feesClient != nil {
|
||||
opts = append(opts, orchestrator.WithFeeEngine(deps.feesClient, cfg.Fees.callTimeout()))
|
||||
}
|
||||
if deps.ledgerClient != nil {
|
||||
opts = append(opts, orchestrator.WithLedgerClient(deps.ledgerClient))
|
||||
}
|
||||
@@ -49,16 +40,12 @@ func (i *Imp) buildServiceOptions(cfg *config, deps *orchestratorDeps) []orchest
|
||||
opts = append(opts, orchestrator.WithMntxGateway(deps.mntxClient))
|
||||
}
|
||||
|
||||
opts = append(opts, orchestrator.WithMaxFXQuoteTTLMillis(cfg.maxFXQuoteTTLMillis()))
|
||||
if deps.gatewayInvokeResolver != nil {
|
||||
opts = append(opts, orchestrator.WithGatewayInvokeResolver(deps.gatewayInvokeResolver))
|
||||
}
|
||||
if routes := buildCardGatewayRoutes(cfg.CardGateways); len(routes) > 0 {
|
||||
opts = append(opts, orchestrator.WithCardGatewayRoutes(routes))
|
||||
}
|
||||
if feeAccounts := buildFeeLedgerAccounts(cfg.FeeAccounts); len(feeAccounts) > 0 {
|
||||
opts = append(opts, orchestrator.WithFeeLedgerAccounts(feeAccounts))
|
||||
}
|
||||
if registry := buildGatewayRegistry(i.logger, cfg.GatewayInstances, i.discoveryReg); registry != nil {
|
||||
opts = append(opts, orchestrator.WithGatewayRegistry(registry))
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func (i *Imp) initDiscovery(cfg *config) {
|
||||
}
|
||||
announce := discovery.Announcement{
|
||||
Service: "PAYMENTS_ORCHESTRATOR",
|
||||
Operations: []string{"payment.initiate"},
|
||||
Operations: []string{discovery.OperationPaymentInitiate},
|
||||
InvokeURI: cfg.GRPC.DiscoveryInvokeURI(),
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ var (
|
||||
ledgerServiceNames = []string{"LEDGER", string(mservice.Ledger)}
|
||||
oracleServiceNames = []string{"FX_ORACLE", string(mservice.FXOracle)}
|
||||
mntxServiceNames = []string{"CARD_RAIL_GATEWAY", string(mservice.MntxGateway)}
|
||||
|
||||
feesRequiredOps = []string{discovery.OperationFeeCalc}
|
||||
ledgerRequiredOps = discovery.LedgerServiceOperations()
|
||||
oracleRequiredOps = []string{discovery.OperationFXQuote}
|
||||
mntxRequiredOps = discovery.CardPayoutRailGatewayOperations()
|
||||
)
|
||||
|
||||
type discoveryEndpoint struct {
|
||||
@@ -109,27 +114,27 @@ func (r *discoveryClientResolver) Close() {
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) FeesAvailable() bool {
|
||||
_, ok := r.findEntry("fees", feesServiceNames, "", "")
|
||||
_, ok := r.findEntry("fees", feesServiceNames, "", "", feesRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) LedgerAvailable() bool {
|
||||
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
|
||||
_, ok := r.findEntry("ledger", ledgerServiceNames, "", "", ledgerRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) OracleAvailable() bool {
|
||||
_, ok := r.findEntry("oracle", oracleServiceNames, "", "")
|
||||
_, ok := r.findEntry("oracle", oracleServiceNames, "", "", oracleRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) MntxAvailable() bool {
|
||||
_, ok := r.findEntry("mntx", mntxServiceNames, "", "")
|
||||
_, ok := r.findEntry("mntx", mntxServiceNames, "", "", mntxRequiredOps)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEngineClient, error) {
|
||||
entry, ok := r.findEntry("fees", feesServiceNames, "", "")
|
||||
entry, ok := r.findEntry("fees", feesServiceNames, "", "", feesRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: fees service unavailable")
|
||||
}
|
||||
@@ -160,7 +165,7 @@ func (r *discoveryClientResolver) FeesClient(ctx context.Context) (feesv1.FeeEng
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclient.Client, error) {
|
||||
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "")
|
||||
entry, ok := r.findEntry("ledger", ledgerServiceNames, "", "", ledgerRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: ledger service unavailable")
|
||||
}
|
||||
@@ -194,7 +199,7 @@ func (r *discoveryClientResolver) LedgerClient(ctx context.Context) (ledgerclien
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclient.Client, error) {
|
||||
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "")
|
||||
entry, ok := r.findEntry("oracle", oracleServiceNames, "", "", oracleRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: oracle service unavailable")
|
||||
}
|
||||
@@ -228,7 +233,7 @@ func (r *discoveryClientResolver) OracleClient(ctx context.Context) (oracleclien
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) MntxClient(ctx context.Context) (mntxclient.Client, error) {
|
||||
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "")
|
||||
entry, ok := r.findEntry("mntx", mntxServiceNames, "", "", mntxRequiredOps)
|
||||
if !ok {
|
||||
return nil, merrors.NoData("discovery: mntx service unavailable")
|
||||
}
|
||||
@@ -316,14 +321,19 @@ func (r *discoveryClientResolver) PaymentGatewayClient(ctx context.Context, invo
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string) (*discovery.RegistryEntry, bool) {
|
||||
func (r *discoveryClientResolver) findEntry(key string, services []string, rail string, network string, requiredOps []string) (*discovery.RegistryEntry, bool) {
|
||||
if r == nil || r.registry == nil {
|
||||
r.logMissing(key, "discovery registry unavailable", "", nil)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type discoveryMatch struct {
|
||||
entry discovery.RegistryEntry
|
||||
opMatch bool
|
||||
}
|
||||
|
||||
entries := r.registry.List(time.Now(), true)
|
||||
matches := make([]discovery.RegistryEntry, 0)
|
||||
matches := make([]discoveryMatch, 0)
|
||||
for _, entry := range entries {
|
||||
if !matchesService(entry.Service, services) {
|
||||
continue
|
||||
@@ -334,7 +344,10 @@ func (r *discoveryClientResolver) findEntry(key string, services []string, rail
|
||||
if network != "" && !strings.EqualFold(strings.TrimSpace(entry.Network), network) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, entry)
|
||||
matches = append(matches, discoveryMatch{
|
||||
entry: entry,
|
||||
opMatch: discovery.HasAnyOperation(entry.Operations, requiredOps),
|
||||
})
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
@@ -343,16 +356,19 @@ func (r *discoveryClientResolver) findEntry(key string, services []string, rail
|
||||
}
|
||||
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
if matches[i].RoutingPriority != matches[j].RoutingPriority {
|
||||
return matches[i].RoutingPriority > matches[j].RoutingPriority
|
||||
if matches[i].opMatch != matches[j].opMatch {
|
||||
return matches[i].opMatch
|
||||
}
|
||||
if matches[i].ID != matches[j].ID {
|
||||
return matches[i].ID < matches[j].ID
|
||||
if matches[i].entry.RoutingPriority != matches[j].entry.RoutingPriority {
|
||||
return matches[i].entry.RoutingPriority > matches[j].entry.RoutingPriority
|
||||
}
|
||||
return matches[i].InstanceID < matches[j].InstanceID
|
||||
if matches[i].entry.ID != matches[j].entry.ID {
|
||||
return matches[i].entry.ID < matches[j].entry.ID
|
||||
}
|
||||
return matches[i].entry.InstanceID < matches[j].entry.InstanceID
|
||||
})
|
||||
|
||||
entry := matches[0]
|
||||
entry := matches[0].entry
|
||||
entryKey := discoveryEntryKey(entry)
|
||||
r.logSelection(key, entryKey, entry)
|
||||
return &entry, true
|
||||
|
||||
@@ -134,6 +134,22 @@ func (c *discoveryLedgerClient) PostDebitWithCharges(ctx context.Context, req *l
|
||||
return client.PostDebitWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) PostExternalCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PostExternalCreditWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) PostExternalDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.PostExternalDebitWithCharges(ctx, req)
|
||||
}
|
||||
|
||||
func (c *discoveryLedgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) {
|
||||
client, err := c.resolver.LedgerClient(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -62,17 +62,42 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transferReq := &ledgerv1.TransferRequest{
|
||||
IdempotencyKey: ledgerStepIdempotencyKey(req.Payment, req.Step),
|
||||
OrganizationRef: req.Payment.OrganizationRef.Hex(),
|
||||
Money: amount,
|
||||
Description: ledgerDescription(req.Step),
|
||||
Metadata: ledgerTransferMetadata(req.Payment, req.Step, roles),
|
||||
FromRole: ledgerRoleToProto(roles.from),
|
||||
ToRole: ledgerRoleToProto(roles.to),
|
||||
}
|
||||
idempotencyKey := ledgerStepIdempotencyKey(req.Payment, req.Step)
|
||||
organizationRef := req.Payment.OrganizationRef.Hex()
|
||||
description := ledgerDescription(req.Step)
|
||||
metadata := ledgerTransferMetadata(req.Payment, req.Step, roles)
|
||||
|
||||
resp, err := e.ledgerClient.TransferInternal(ctx, transferReq)
|
||||
var resp *ledgerv1.PostResponse
|
||||
switch action {
|
||||
case model.RailOperationExternalCredit:
|
||||
resp, err = e.ledgerClient.PostExternalCreditWithCharges(ctx, &ledgerv1.PostCreditRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
Role: ledgerRoleToProto(roles.to),
|
||||
})
|
||||
case model.RailOperationExternalDebit:
|
||||
resp, err = e.ledgerClient.PostExternalDebitWithCharges(ctx, &ledgerv1.PostDebitRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
Role: ledgerRoleToProto(roles.from),
|
||||
})
|
||||
default:
|
||||
resp, err = e.ledgerClient.TransferInternal(ctx, &ledgerv1.TransferRequest{
|
||||
IdempotencyKey: idempotencyKey,
|
||||
OrganizationRef: organizationRef,
|
||||
Money: amount,
|
||||
Description: description,
|
||||
Metadata: metadata,
|
||||
FromRole: ledgerRoleToProto(roles.from),
|
||||
ToRole: ledgerRoleToProto(roles.to),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -80,6 +80,146 @@ func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRol
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditUsesPostCreditWithCharges(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
var (
|
||||
postReq *ledgerv1.PostCreditRequest
|
||||
transferCalled bool
|
||||
)
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalCreditWithChargesFn: func(_ context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-credit"}, nil
|
||||
},
|
||||
TransferInternalFn: func(_ context.Context, _ *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
transferCalled = true
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-transfer"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
|
||||
Payment: payment,
|
||||
Step: xplan.Step{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Action: model.RailOperationExternalCredit,
|
||||
Rail: model.RailLedger,
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "edge_1_2_ledger_credit",
|
||||
StepCode: "edge.1_2.ledger.credit",
|
||||
Attempt: 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteLedger returned error: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
t.Fatal("expected output")
|
||||
}
|
||||
if postReq == nil {
|
||||
t.Fatal("expected external credit request")
|
||||
}
|
||||
if transferCalled {
|
||||
t.Fatal("expected external credit to skip transfer")
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetAmount(), "1.000000"; got != want {
|
||||
t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetCurrency(), "USDT"; got != want {
|
||||
t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want {
|
||||
t.Fatalf("role mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
|
||||
t.Fatalf("state mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if len(out.StepExecution.ExternalRefs) != 1 {
|
||||
t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
|
||||
}
|
||||
if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-ext-credit"; got != want {
|
||||
t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_ExternalDebitUsesPostDebitWithCharges(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
var (
|
||||
postReq *ledgerv1.PostDebitRequest
|
||||
transferCalled bool
|
||||
)
|
||||
executor := &gatewayLedgerExecutor{
|
||||
ledgerClient: &ledgerclient.Fake{
|
||||
PostExternalDebitWithChargesFn: func(_ context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) {
|
||||
postReq = req
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-ext-debit"}, nil
|
||||
},
|
||||
TransferInternalFn: func(_ context.Context, _ *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) {
|
||||
transferCalled = true
|
||||
return &ledgerv1.PostResponse{JournalEntryRef: "entry-transfer"}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{
|
||||
Payment: payment,
|
||||
Step: xplan.Step{
|
||||
StepRef: "edge_3_4_ledger_debit",
|
||||
StepCode: "edge.3_4.ledger.debit",
|
||||
Action: model.RailOperationExternalDebit,
|
||||
Rail: model.RailLedger,
|
||||
Metadata: map[string]string{
|
||||
"mode": "finalize_debit",
|
||||
},
|
||||
},
|
||||
StepExecution: agg.StepExecution{
|
||||
StepRef: "edge_3_4_ledger_debit",
|
||||
StepCode: "edge.3_4.ledger.debit",
|
||||
Attempt: 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteLedger returned error: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
t.Fatal("expected output")
|
||||
}
|
||||
if postReq == nil {
|
||||
t.Fatal("expected external debit request")
|
||||
}
|
||||
if transferCalled {
|
||||
t.Fatal("expected external debit to skip transfer")
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetAmount(), "76.5"; got != want {
|
||||
t.Fatalf("money.amount mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetMoney().GetCurrency(), "RUB"; got != want {
|
||||
t.Fatalf("money.currency mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := postReq.GetRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want {
|
||||
t.Fatalf("role mismatch: got=%v want=%v", got, want)
|
||||
}
|
||||
if got, want := postReq.GetMetadata()[ledgerMetadataMode], "finalize_debit"; got != want {
|
||||
t.Fatalf("mode metadata mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want {
|
||||
t.Fatalf("state mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if len(out.StepExecution.ExternalRefs) != 1 {
|
||||
t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs))
|
||||
}
|
||||
if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-ext-debit"; got != want {
|
||||
t.Fatalf("external ref value mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayLedgerExecutor_ExecuteLedger_FinalizeDebitUsesHoldToTransitAndSettlementAmount(t *testing.T) {
|
||||
orgID := bson.NewObjectID()
|
||||
payment := testLedgerExecutorPayment(orgID)
|
||||
|
||||
@@ -52,7 +52,7 @@ func (i *Imp) startDiscoveryAnnouncer(cfg *config, producer msg.Producer) {
|
||||
|
||||
announce := discovery.Announcement{
|
||||
Service: "PAYMENTS_QUOTATION",
|
||||
Operations: []string{"payment.quote", "payment.multiquote"},
|
||||
Operations: []string{discovery.OperationPaymentQuote, discovery.OperationPaymentMultiQuote},
|
||||
InvokeURI: invokeURI,
|
||||
Version: appversion.Create().Short(),
|
||||
}
|
||||
|
||||
@@ -197,18 +197,6 @@ func (r *managedWalletNetworkResolver) listDiscoveredGatewayCandidates(ctx conte
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func managedWalletNetworkFromResponse(resp *chainv1.GetManagedWalletResponse) (string, error) {
|
||||
asset, err := managedWalletAssetFromResponse(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
network := strings.ToUpper(strings.TrimSpace(asset.GetChain()))
|
||||
if network == "" || network == "UNSPECIFIED" {
|
||||
return "", merrors.NoData("managed wallet network is missing")
|
||||
}
|
||||
return network, nil
|
||||
}
|
||||
|
||||
func managedWalletAssetFromResponse(resp *chainv1.GetManagedWalletResponse) (*paymenttypes.Asset, error) {
|
||||
wallet := resp.GetWallet()
|
||||
if wallet == nil || wallet.GetAsset() == nil {
|
||||
|
||||
@@ -8,7 +8,3 @@ import (
|
||||
func railFromEndpoint(endpoint model.PaymentEndpoint, attrs map[string]string, isSource bool) (model.Rail, string, error) {
|
||||
return plan.RailFromEndpoint(endpoint, attrs, isSource)
|
||||
}
|
||||
|
||||
func resolveRouteNetwork(attrs map[string]string, sourceNetwork, destNetwork string) (string, error) {
|
||||
return plan.ResolveRouteNetwork(attrs, sourceNetwork, destNetwork)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,8 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *quo
|
||||
|
||||
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
|
||||
s.logger.Warn("Fees precompute failed: fee engine unavailable")
|
||||
return nil, merrors.Internal("fees_precompute_failed")
|
||||
}
|
||||
intent := req.GetIntent()
|
||||
amount := cloneProtoMoney(baseAmount)
|
||||
@@ -188,7 +189,8 @@ func (s *Service) quoteFees(ctx context.Context, orgRef string, req *quoteReques
|
||||
|
||||
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
|
||||
s.logger.Warn("Conversion fee precompute failed: fee engine unavailable")
|
||||
return nil, merrors.Internal("fees_precompute_failed")
|
||||
}
|
||||
intent := req.GetIntent()
|
||||
amount := cloneProtoMoney(baseAmount)
|
||||
|
||||
@@ -2,6 +2,7 @@ package quotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -152,9 +153,89 @@ func TestBuildPaymentQuote_DoesNotRequestConversionFeesForManagedWalletToLedger(
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ReturnsErrorWhenFeeEngineUnavailable(t *testing.T) {
|
||||
svc := NewService(zap.NewNop(), nil)
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToCardIntent(),
|
||||
}
|
||||
|
||||
_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fees_precompute_failed") {
|
||||
t.Fatalf("expected fees_precompute_failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ReturnsErrorWhenBaseFeePrecomputeFails(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeErrByOrigin: map[string]error{
|
||||
"payments.orchestrator.quote": merrors.Internal("billing_fees_unreachable"),
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToLedgerIntent(),
|
||||
}
|
||||
|
||||
_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fees_precompute_failed") {
|
||||
t.Fatalf("expected fees_precompute_failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPaymentQuote_ReturnsErrorWhenConversionFeePrecomputeFails(t *testing.T) {
|
||||
feeClient := &stubFeeEngineClient{
|
||||
precomputeByOrigin: map[string]*feesv1.PrecomputeFeesResponse{
|
||||
"payments.orchestrator.quote": {
|
||||
Lines: []*feesv1.DerivedPostingLine{
|
||||
testFeeLine("1.00", "USDT"),
|
||||
},
|
||||
},
|
||||
},
|
||||
precomputeErrByOrigin: map[string]error{
|
||||
"payments.orchestrator.conversion_quote": merrors.Internal("billing_fees_unreachable"),
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService(zap.NewNop(), nil, WithFeeEngine(feeClient, 0))
|
||||
req := "eRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: "org_1"},
|
||||
IdempotencyKey: "idem_1",
|
||||
Intent: testManagedWalletToCardIntent(),
|
||||
}
|
||||
|
||||
_, _, err := svc.buildPaymentQuote(context.Background(), "org_1", req)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
if !errors.Is(err, merrors.ErrInternal) {
|
||||
t.Fatalf("expected internal error, got: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fees_precompute_failed") {
|
||||
t.Fatalf("expected fees_precompute_failed error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type stubFeeEngineClient struct {
|
||||
precomputeByOrigin map[string]*feesv1.PrecomputeFeesResponse
|
||||
precomputeReqs []*feesv1.PrecomputeFeesRequest
|
||||
precomputeByOrigin map[string]*feesv1.PrecomputeFeesResponse
|
||||
precomputeErrByOrigin map[string]error
|
||||
precomputeReqs []*feesv1.PrecomputeFeesRequest
|
||||
}
|
||||
|
||||
func (s *stubFeeEngineClient) QuoteFees(context.Context, *feesv1.QuoteFeesRequest, ...grpc.CallOption) (*feesv1.QuoteFeesResponse, error) {
|
||||
@@ -177,6 +258,9 @@ func (s *stubFeeEngineClient) PrecomputeFees(_ context.Context, in *feesv1.Preco
|
||||
}
|
||||
|
||||
originType := strings.TrimSpace(in.GetIntent().GetOriginType())
|
||||
if err := s.precomputeErrByOrigin[originType]; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, ok := s.precomputeByOrigin[originType]
|
||||
if !ok || resp == nil {
|
||||
return &feesv1.PrecomputeFeesResponse{}, nil
|
||||
|
||||
Reference in New Issue
Block a user