From 15b03b1bc855194574b9dc637eaea34ec3e22409 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 11 Mar 2026 14:20:50 +0100 Subject: [PATCH] explicit fee execution --- Makefile | 106 ++++++++++---- .../xplan/compile_flow_test.go | 102 +++++++++++++ .../orchestrationv2/xplan/expansion.go | 24 ++++ .../orchestrationv2/xplan/fee_planning.go | 128 +++++++++++++++++ .../service/orchestrationv2/xplan/service.go | 13 +- .../xplan/service_boundaries.go | 27 +++- .../orchestrationv2/xplan/service_policy.go | 2 +- .../service/orchestrator/crypto_executor.go | 63 -------- .../orchestrator/crypto_executor_test.go | 94 ++++-------- docker-compose.dev.yml | 134 +++++++++++++++--- 10 files changed, 511 insertions(+), 182 deletions(-) create mode 100644 api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go diff --git a/Makefile b/Makefile index 9f02d03c..4c0794f8 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev SERVICE ?= BACKEND_GOCACHE ?= $(CURDIR)/.gocache BACKEND_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache +BACKEND_WAIT_TIMEOUT ?= 600 BACKEND_SERVICES := \ dev-discovery \ dev-fx-oracle \ @@ -64,6 +65,19 @@ BACKEND_SERVICES := \ dev-callbacks \ dev-bff-vault-agent \ dev-bff +BACKEND_STAGE_DISCOVERY := dev-discovery +BACKEND_STAGE_FX := dev-fx-oracle dev-fx-ingestor +BACKEND_STAGE_BILLING := dev-billing-fees dev-billing-documents +BACKEND_STAGE_GATEWAY_SIDECARS := dev-chain-gateway-vault-agent dev-tron-gateway-vault-agent +BACKEND_STAGE_GATEWAYS_LEDGER := dev-chain-gateway dev-tron-gateway dev-aurora-gateway dev-chsettle-gateway dev-ledger +BACKEND_STAGE_QUOTATION := dev-payments-quotation +BACKEND_STAGE_PAYMENTS_CORE := dev-payments-methods dev-payments-orchestrator +BACKEND_STAGE_CALLBACKS_AGENT := dev-callbacks-vault-agent +BACKEND_STAGE_CALLBACKS := dev-callbacks +BACKEND_STAGE_EDGE_FOUNDATION := dev-notification dev-bff-vault-agent +BACKEND_STAGE_EDGE := dev-bff +BACKEND_STAGE_EDGE_BUILD := dev-notification dev-bff +FRONTEND_SERVICE := dev-frontend # Colors GREEN := \033[0;32m @@ -89,10 +103,10 @@ help: @echo "" @echo "$(YELLOW)Selective Operations:$(NC)" @echo " make infra-up Start infrastructure only (mongo, nats, vault)" - @echo " make services-up Start application services only" - @echo " make backend-up Start backend services only (no infrastructure/frontend)" + @echo " make services-up Start application services only (ordered backend stages)" + @echo " make backend-up Start backend services only in ordered stages (no infrastructure/frontend)" @echo " make backend-down Stop backend services only" - @echo " make backend-rebuild Rebuild and restart backend services only" + @echo " make backend-rebuild Rebuild and restart backend services in ordered stages" @echo " make list-services List all available services" @echo "" @echo "$(YELLOW)Build Groups:$(NC)" @@ -166,7 +180,8 @@ build: # Start all services up: @echo "$(GREEN)Starting development environment...$(NC)" - @$(COMPOSE) up -d + @$(MAKE) --no-print-directory infra-up + @$(MAKE) --no-print-directory services-up @echo "" @echo "$(GREEN)✅ Development environment started!$(NC)" @echo "" @@ -272,39 +287,74 @@ infra-up: # Services only (assumes infra is running) services-up: - @echo "$(GREEN)Starting application services...$(NC)" - @$(COMPOSE) up -d \ - dev-discovery \ - dev-fx-oracle \ - dev-fx-ingestor \ - dev-billing-fees \ - dev-billing-documents \ - dev-ledger \ - dev-payments-orchestrator \ - dev-payments-quotation \ - dev-payments-methods \ - dev-chain-gateway \ - dev-tron-gateway \ - dev-aurora-gateway \ - dev-chsettle-gateway \ - dev-notification \ - dev-callbacks \ - dev-bff \ - dev-frontend + @echo "$(GREEN)Starting application services with ordered backend stages...$(NC)" + @$(MAKE) --no-print-directory backend-up + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(FRONTEND_SERVICE) # Backend services only (no infrastructure, no frontend) backend-up: - @echo "$(GREEN)Starting backend services only (no infra changes)...$(NC)" - @$(COMPOSE) up -d --no-deps $(BACKEND_SERVICES) + @echo "$(GREEN)Starting backend services only (ordered build+start stages, no infra changes)...$(NC)" + @echo "$(YELLOW)[1/8] discovery$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_DISCOVERY) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_DISCOVERY) + @echo "$(YELLOW)[2/8] fx (oracle + ingestor)$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_FX) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_FX) + @echo "$(YELLOW)[3/8] billing (fees + documents)$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_BILLING) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_BILLING) + @echo "$(YELLOW)[4/8] gateways + ledger$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_GATEWAYS_LEDGER) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAY_SIDECARS) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAYS_LEDGER) + @echo "$(YELLOW)[5/8] quotation$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_QUOTATION) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_QUOTATION) + @echo "$(YELLOW)[6/8] orchestrator + methods$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_PAYMENTS_CORE) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_PAYMENTS_CORE) + @echo "$(YELLOW)[7/8] callbacks$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_CALLBACKS) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS_AGENT) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS) + @echo "$(YELLOW)[8/8] edge (notification + bff)$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_EDGE_BUILD) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE_FOUNDATION) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE) backend-down: @echo "$(YELLOW)Stopping backend services only...$(NC)" @$(COMPOSE) stop $(BACKEND_SERVICES) backend-rebuild: - @echo "$(GREEN)Rebuilding backend services only (no infra changes)...$(NC)" - @$(COMPOSE) build $(BACKEND_SERVICES) - @$(COMPOSE) up -d --no-deps --force-recreate $(BACKEND_SERVICES) + @echo "$(GREEN)Rebuilding backend services only (ordered stages, no infra changes)...$(NC)" + @echo "$(YELLOW)[1/8] discovery$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_DISCOVERY) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_DISCOVERY) + @echo "$(YELLOW)[2/8] fx (oracle + ingestor)$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_FX) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_FX) + @echo "$(YELLOW)[3/8] billing (fees + documents)$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_BILLING) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_BILLING) + @echo "$(YELLOW)[4/8] gateways + ledger$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_GATEWAYS_LEDGER) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAY_SIDECARS) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAYS_LEDGER) + @echo "$(YELLOW)[5/8] quotation$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_QUOTATION) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_QUOTATION) + @echo "$(YELLOW)[6/8] orchestrator + methods$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_PAYMENTS_CORE) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_PAYMENTS_CORE) + @echo "$(YELLOW)[7/8] callbacks$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_CALLBACKS) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS_AGENT) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS) + @echo "$(YELLOW)[8/8] edge (notification + bff)$(NC)" + @$(COMPOSE) build $(BACKEND_STAGE_EDGE_BUILD) + @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE_FOUNDATION) + @$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE) @echo "$(GREEN)✅ Backend services rebuilt$(NC)" # Status check diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go index ce07b53c..37f276a6 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go @@ -103,6 +103,108 @@ func TestCompile_ExternalToExternal_BridgeExpansion(t *testing.T) { } } +func TestCompile_ExternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) { + compiler := New() + + graph, err := compiler.Compile(Input{ + IntentSnapshot: testIntent(model.PaymentKindPayout), + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + DebitAmount: &paymenttypes.Money{Amount: "5.842042", Currency: "USDT"}, + FeeLines: []*paymenttypes.FeeLine{ + { + Money: &paymenttypes.Money{Amount: "0.42", Currency: "USDT"}, + Side: paymenttypes.EntrySideDebit, + Meta: map[string]string{"fee_target": "wallet"}, + }, + }, + Route: &paymenttypes.QuoteRouteSpecification{ + RouteRef: "route-1", + Hops: []*paymenttypes.QuoteRouteHop{ + { + Index: 10, + Rail: "CRYPTO", + Gateway: "gw-crypto", + InstanceID: "crypto-1", + Role: paymenttypes.QuoteRouteHopRoleSource, + }, + { + Index: 20, + Rail: "CARD", + Gateway: "gw-card", + InstanceID: "card-1", + Role: paymenttypes.QuoteRouteHopRoleDestination, + }, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Compile returned error: %v", err) + } + if graph == nil { + t.Fatal("expected graph") + } + if got, want := len(graph.Steps), 10; got != want { + t.Fatalf("expected 10 steps, got %d", got) + } + + assertStep(t, graph.Steps[0], "hop.10.crypto.send", discovery.RailOperationSend, discovery.RailCrypto, model.ReportVisibilityBackoffice) + assertStep(t, graph.Steps[1], "hop.10.crypto.fee", discovery.RailOperationFee, discovery.RailCrypto, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[2], "hop.10.crypto.observe", discovery.RailOperationObserveConfirm, discovery.RailCrypto, model.ReportVisibilityBackoffice) + assertStep(t, graph.Steps[3], "edge.10_20.ledger.credit", discovery.RailOperationExternalCredit, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[4], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[5], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser) + assertStep(t, graph.Steps[6], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser) + assertStep(t, graph.Steps[7], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[8], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[9], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden) + + if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) { + t.Fatalf("fee deps mismatch: got=%v want=%v", got, want) + } + if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) { + t.Fatalf("observe deps mismatch: got=%v want=%v", got, want) + } + if got, want := graph.Steps[3].DependsOn, []string{graph.Steps[2].StepRef}; !equalStringSlice(got, want) { + t.Fatalf("credit deps mismatch: got=%v want=%v", got, want) + } +} + +func TestCompile_ExternalToExternal_IgnoresNonWalletFeeLines(t *testing.T) { + compiler := New() + + graph, err := compiler.Compile(Input{ + IntentSnapshot: testIntent(model.PaymentKindPayout), + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + DebitAmount: &paymenttypes.Money{Amount: "5.842042", Currency: "USDT"}, + FeeLines: []*paymenttypes.FeeLine{ + { + Money: &paymenttypes.Money{Amount: "0.42", Currency: "USDT"}, + Side: paymenttypes.EntrySideDebit, + Meta: map[string]string{"fee_target": "ledger"}, + }, + }, + Route: &paymenttypes.QuoteRouteSpecification{ + Hops: []*paymenttypes.QuoteRouteHop{ + {Index: 10, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource}, + {Index: 20, Rail: "CARD", Role: paymenttypes.QuoteRouteHopRoleDestination}, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Compile returned error: %v", err) + } + if got, want := len(graph.Steps), 9; got != want { + t.Fatalf("expected 9 steps, got %d", got) + } + for i := range graph.Steps { + if got := graph.Steps[i].Action; got == discovery.RailOperationFee { + t.Fatalf("unexpected fee step at index %d: %+v", i, graph.Steps[i]) + } + } +} + func TestCompile_InternalToExternal_UsesHoldAndSettlementBranches(t *testing.T) { compiler := New() diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go index 84bb36ef..66606bce 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go @@ -51,6 +51,30 @@ func (e *expansion) nextRef(base string) string { return token + "_" + itoa(count+1) } +func (e *expansion) needsWalletFeeStep(hop normalizedHop) bool { + if e == nil || len(e.walletFeeHops) == 0 { + return false + } + key := observedKey(hop) + if _, ok := e.walletFeeHops[key]; !ok { + return false + } + if _, emitted := e.walletFeeEmitted[key]; emitted { + return false + } + return true +} + +func (e *expansion) markWalletFeeEmitted(hop normalizedHop) { + if e == nil { + return + } + if e.walletFeeEmitted == nil { + e.walletFeeEmitted = map[string]struct{}{} + } + e.walletFeeEmitted[observedKey(hop)] = struct{}{} +} + func normalizeStep(step Step) Step { step.StepRef = strings.TrimSpace(step.StepRef) step.StepCode = strings.TrimSpace(step.StepCode) diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go new file mode 100644 index 00000000..617fb4ff --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go @@ -0,0 +1,128 @@ +package xplan + +import ( + "fmt" + "strings" + + "github.com/shopspring/decimal" + "github.com/tech/sendico/pkg/discovery" + + "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/merrors" + paymenttypes "github.com/tech/sendico/pkg/payments/types" +) + +func planWalletFeeHops( + hops []normalizedHop, + intent model.PaymentIntent, + quote *model.PaymentQuoteSnapshot, +) (map[string]struct{}, error) { + _, hasWalletFee, err := walletFeeAmountFromSnapshots(intent, quote) + if err != nil { + return nil, err + } + if !hasWalletFee { + return nil, nil + } + + sourceHop, ok := sourceCryptoHop(hops) + if !ok { + return nil, nil + } + return map[string]struct{}{ + observedKey(sourceHop): {}, + }, nil +} + +func sourceCryptoHop(hops []normalizedHop) (normalizedHop, bool) { + for i := range hops { + if hops[i].rail == discovery.RailCrypto && hops[i].role == paymenttypes.QuoteRouteHopRoleSource { + return hops[i], true + } + } + if len(hops) > 0 && hops[0].rail == discovery.RailCrypto { + return hops[0], true + } + return normalizedHop{}, false +} + +func walletFeeAmountFromSnapshots( + intent model.PaymentIntent, + quote *model.PaymentQuoteSnapshot, +) (*paymenttypes.Money, bool, error) { + if quote == nil || len(quote.FeeLines) == 0 { + return nil, false, nil + } + + sourceCurrency := "" + if source := feePlanningSourceAmount(intent, quote); source != nil { + sourceCurrency = strings.TrimSpace(source.Currency) + } + + total := decimal.Zero + currency := "" + for i, line := range quote.FeeLines { + if !isWalletDebitFeeLine(line) { + continue + } + money := line.GetMoney() + if money == nil { + continue + } + + lineCurrency := strings.TrimSpace(money.GetCurrency()) + if lineCurrency == "" { + return nil, false, merrors.InvalidArgument(fmt.Sprintf("quote_snapshot.fee_lines[%d].money.currency is required", i)) + } + if sourceCurrency != "" && !strings.EqualFold(sourceCurrency, lineCurrency) { + continue + } + if currency == "" { + currency = lineCurrency + } else if !strings.EqualFold(currency, lineCurrency) { + return nil, false, merrors.InvalidArgument("quote_snapshot.fee_lines wallet fee currency mismatch") + } + + amountRaw := strings.TrimSpace(money.GetAmount()) + amount, err := decimal.NewFromString(amountRaw) + if err != nil { + return nil, false, merrors.InvalidArgument(fmt.Sprintf("quote_snapshot.fee_lines[%d].money.amount is invalid", i)) + } + if amount.Sign() < 0 { + amount = amount.Neg() + } + if amount.Sign() == 0 { + continue + } + total = total.Add(amount) + } + + if total.Sign() <= 0 { + return nil, false, nil + } + return &paymenttypes.Money{ + Amount: total.String(), + Currency: strings.ToUpper(strings.TrimSpace(currency)), + }, true, nil +} + +func feePlanningSourceAmount(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money { + if quote != nil && quote.DebitAmount != nil { + return quote.DebitAmount + } + return intent.Amount +} + +func isWalletDebitFeeLine(line *paymenttypes.FeeLine) bool { + if line == nil { + return false + } + if line.GetSide() != paymenttypes.EntrySideDebit { + return false + } + meta := line.Meta + if len(meta) == 0 { + return false + } + return strings.EqualFold(strings.TrimSpace(meta["fee_target"]), "wallet") +} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go index 405b9d9b..1d87372d 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go @@ -30,12 +30,16 @@ type expansion struct { lastMainRef string refSeq map[string]int externalObserved map[string]string + walletFeeHops map[string]struct{} + walletFeeEmitted map[string]struct{} } -func newExpansion() *expansion { +func newExpansion(walletFeeHops map[string]struct{}) *expansion { return &expansion{ refSeq: map[string]int{}, externalObserved: map[string]string{}, + walletFeeHops: walletFeeHops, + walletFeeEmitted: map[string]struct{}{}, } } @@ -86,7 +90,12 @@ func (s *svc) Compile(in Input) (graph *Graph, err error) { return nil, err } - ex := newExpansion() + walletFeeHops, err := planWalletFeeHops(hops, in.IntentSnapshot, in.QuoteSnapshot) + if err != nil { + return nil, err + } + + ex := newExpansion(walletFeeHops) appendGuards(ex, conditions) if len(hops) == 1 { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go index 741a0b2f..cd6e7b56 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go @@ -147,10 +147,19 @@ func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent mo sendStep := makeRailSendStep(hop, intent) sendRef := ex.appendMain(sendStep) + observeDependency := sendRef + if ex.needsWalletFeeStep(hop) { + feeStep := makeRailFeeStep(hop) + if observeDependency != "" { + feeStep.DependsOn = []string{observeDependency} + } + observeDependency = ex.appendMain(feeStep) + ex.markWalletFeeEmitted(hop) + } observeStep := makeRailObserveStep(hop, intent) - if sendRef != "" { - observeStep.DependsOn = []string{sendRef} + if observeDependency != "" { + observeStep.DependsOn = []string{observeDependency} } observeRef := ex.appendMain(observeStep) @@ -158,6 +167,20 @@ func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent mo return observeRef, nil } +func makeRailFeeStep(hop normalizedHop) Step { + return Step{ + StepCode: singleHopCode(hop, "fee"), + Kind: StepKindRailSend, + Action: discovery.RailOperationFee, + Rail: hop.rail, + Gateway: hop.gateway, + InstanceID: hop.instanceID, + HopIndex: hop.index, + HopRole: hop.role, + Visibility: model.ReportVisibilityHidden, + } +} + func appendGuards(ex *expansion, conditions *paymenttypes.QuoteExecutionConditions) { if conditions == nil { return diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_policy.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_policy.go index d7533fc1..17b6173e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_policy.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_policy.go @@ -235,7 +235,7 @@ func resolveStepContext( func kindForAction(action model.RailOperation) StepKind { switch action { - case discovery.RailOperationSend, discovery.RailOperationFXConvert: + case discovery.RailOperationSend, discovery.RailOperationFXConvert, discovery.RailOperationFee: return StepKindRailSend case discovery.RailOperationObserveConfirm: return StepKindRailObserve diff --git a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go index ed207ac6..c7a9c6d9 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go +++ b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go @@ -99,11 +99,6 @@ func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.Ste } step.ConvertedMoney = nil - if action == discovery.RailOperationSend { - if err := e.submitWalletFeeTransfer(ctx, req, client, gateway, sourceWalletRef, operationRef, idempotencyKey); err != nil { - return nil, err - } - } step.State = agg.StepStateCompleted step.FailureCode = "" step.FailureMsg = "" @@ -207,64 +202,6 @@ func sourceAmount(payment *agg.Payment, action model.RailOperation) (*moneyv1.Mo }, nil } -func (e *gatewayCryptoExecutor) submitWalletFeeTransfer( - ctx context.Context, - req sexec.StepRequest, - client chainclient.Client, - gateway *model.GatewayInstanceDescriptor, - sourceWalletRef string, - operationRef string, - idempotencyKey string, -) error { - if req.Payment == nil { - return merrors.InvalidArgument("crypto send: payment is required") - } - - feeAmount, ok, err := walletFeeAmount(req.Payment) - if err != nil { - return err - } - if !ok { - return nil - } - - destination, err := e.resolveDestination(ctx, client, req.Payment, discovery.RailOperationFee) - if err != nil { - return err - } - feeMoney := &moneyv1.Money{ - Amount: strings.TrimSpace(feeAmount.GetAmount()), - Currency: strings.TrimSpace(feeAmount.GetCurrency()), - } - - resp, err := client.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{ - IdempotencyKey: strings.TrimSpace(idempotencyKey) + ":fee", - OrganizationRef: req.Payment.OrganizationRef.Hex(), - SourceWalletRef: sourceWalletRef, - Destination: destination, - Amount: feeMoney, - OperationRef: strings.TrimSpace(operationRef) + ":fee", - IntentRef: strings.TrimSpace(req.Payment.IntentSnapshot.Ref), - PaymentRef: strings.TrimSpace(req.Payment.PaymentRef), - Metadata: transferMetadata(req.Step), - }) - if err != nil { - return err - } - if resp == nil || resp.GetTransfer() == nil { - return merrors.Internal("crypto send: fee transfer response is missing") - } - if _, err := transferExternalRefs(resp.GetTransfer(), firstNonEmpty( - strings.TrimSpace(req.Step.InstanceID), - strings.TrimSpace(gateway.InstanceID), - strings.TrimSpace(req.Step.Gateway), - strings.TrimSpace(gateway.ID), - )); err != nil { - return err - } - return nil -} - func effectiveSourceAmount(payment *agg.Payment) *paymenttypes.Money { if payment == nil { return nil diff --git a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go index e98811b7..cb893055 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go @@ -204,32 +204,19 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_MissingCardRoute(t *testing.T) { } } -func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsWalletFeeTransferOnSend(t *testing.T) { +func TestGatewayCryptoExecutor_ExecuteCrypto_SendDoesNotSubmitWalletFeeTransfer(t *testing.T) { orgID := bson.NewObjectID() - submitRequests := make([]*chainv1.SubmitTransferRequest, 0, 2) + submitRequests := make([]*chainv1.SubmitTransferRequest, 0, 1) client := &chainclient.Fake{ SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) { submitRequests = append(submitRequests, req) - switch len(submitRequests) { - case 1: - return &chainv1.SubmitTransferResponse{ - Transfer: &chainv1.Transfer{ - TransferRef: "trf-principal", - OperationRef: "op-principal", - }, - }, nil - case 2: - return &chainv1.SubmitTransferResponse{ - Transfer: &chainv1.Transfer{ - TransferRef: "trf-fee", - OperationRef: "op-fee", - }, - }, nil - default: - t.Fatalf("unexpected transfer submission call %d", len(submitRequests)) - panic("unreachable") - } + return &chainv1.SubmitTransferResponse{ + Transfer: &chainv1.Transfer{ + TransferRef: "trf-principal", + OperationRef: "op-principal", + }, + }, nil }, } resolver := &fakeGatewayInvokeResolver{client: client} @@ -311,7 +298,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsWalletFeeTransferOnSend(t *t if out == nil { t.Fatal("expected output") } - if got, want := len(submitRequests), 2; got != want { + if got, want := len(submitRequests), 1; got != want { t.Fatalf("submit transfer calls mismatch: got=%d want=%d", got, want) } @@ -322,29 +309,12 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsWalletFeeTransferOnSend(t *t if got, want := principalReq.GetDestination().GetExternalAddress(), "TUA_DEST"; got != want { t.Fatalf("principal destination mismatch: got=%q want=%q", got, want) } - - feeReq := submitRequests[1] - if got, want := feeReq.GetAmount().GetAmount(), "0.7"; got != want { - t.Fatalf("fee amount mismatch: got=%q want=%q", got, want) - } - if got, want := feeReq.GetAmount().GetCurrency(), "USDT"; got != want { - t.Fatalf("fee currency mismatch: got=%q want=%q", got, want) - } - if got, want := feeReq.GetDestination().GetExternalAddress(), "TUA_FEE"; got != want { - t.Fatalf("fee destination mismatch: got=%q want=%q", got, want) - } - if got, want := feeReq.GetOperationRef(), "payment-1:hop_1_crypto_send:fee"; got != want { - t.Fatalf("fee operation_ref mismatch: got=%q want=%q", got, want) - } - if got, want := feeReq.GetIdempotencyKey(), "idem-1:hop_1_crypto_send:fee"; got != want { - t.Fatalf("fee idempotency_key mismatch: got=%q want=%q", got, want) - } } -func TestGatewayCryptoExecutor_ExecuteCrypto_ResolvesFeeAddressFromFeeWalletRef(t *testing.T) { +func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionResolvesFeeAddressFromFeeWalletRef(t *testing.T) { orgID := bson.NewObjectID() - submitRequests := make([]*chainv1.SubmitTransferRequest, 0, 2) + submitRequests := make([]*chainv1.SubmitTransferRequest, 0, 1) var managedWalletReq *chainv1.GetManagedWalletRequest client := &chainclient.Fake{ GetManagedWalletFn: func(_ context.Context, req *chainv1.GetManagedWalletRequest) (*chainv1.GetManagedWalletResponse, error) { @@ -358,25 +328,12 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_ResolvesFeeAddressFromFeeWalletRef( }, SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) { submitRequests = append(submitRequests, req) - switch len(submitRequests) { - case 1: - return &chainv1.SubmitTransferResponse{ - Transfer: &chainv1.Transfer{ - TransferRef: "trf-principal", - OperationRef: "op-principal", - }, - }, nil - case 2: - return &chainv1.SubmitTransferResponse{ - Transfer: &chainv1.Transfer{ - TransferRef: "trf-fee", - OperationRef: "op-fee", - }, - }, nil - default: - t.Fatalf("unexpected transfer submission call %d", len(submitRequests)) - panic("unreachable") - } + return &chainv1.SubmitTransferResponse{ + Transfer: &chainv1.Transfer{ + TransferRef: "trf-fee", + OperationRef: "op-fee", + }, + }, nil }, } resolver := &fakeGatewayInvokeResolver{client: client} @@ -437,16 +394,16 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_ResolvesFeeAddressFromFeeWalletRef( }, }, Step: xplan.Step{ - StepRef: "hop_1_crypto_send", - StepCode: "hop.1.crypto.send", - Action: discovery.RailOperationSend, + StepRef: "hop_1_crypto_fee", + StepCode: "hop.1.crypto.fee", + Action: discovery.RailOperationFee, Rail: discovery.RailCrypto, Gateway: "crypto_rail_gateway_arbitrum_sepolia", InstanceID: "crypto_rail_gateway_arbitrum_sepolia", }, StepExecution: agg.StepExecution{ - StepRef: "hop_1_crypto_send", - StepCode: "hop.1.crypto.send", + StepRef: "hop_1_crypto_fee", + StepCode: "hop.1.crypto.fee", Attempt: 1, }, } @@ -461,10 +418,13 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_ResolvesFeeAddressFromFeeWalletRef( if got, want := managedWalletReq.GetWalletRef(), "fee-wallet-ref"; got != want { t.Fatalf("fee wallet ref lookup mismatch: got=%q want=%q", got, want) } - if got, want := len(submitRequests), 2; got != want { + if got, want := len(submitRequests), 1; got != want { t.Fatalf("submit transfer calls mismatch: got=%d want=%d", got, want) } - feeReq := submitRequests[1] + feeReq := submitRequests[0] + if got, want := feeReq.GetAmount().GetAmount(), "0.7"; got != want { + t.Fatalf("fee amount mismatch: got=%q want=%q", got, want) + } if got, want := feeReq.GetDestination().GetExternalAddress(), "TUA_FEE_FROM_WALLET"; got != want { t.Fatalf("fee destination mismatch: got=%q want=%q", got, want) } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 86bb0faf..6a0e19c0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -248,6 +248,12 @@ services: NATS_PASSWORD: ${NATS_PASSWORD} NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 DISCOVERY_METRICS_PORT: 9407 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9405/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # FX Oracle Service @@ -286,6 +292,12 @@ services: NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 FX_ORACLE_GRPC_PORT: 50051 FX_ORACLE_METRICS_PORT: 9400 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9400/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Billing Fees Service @@ -301,7 +313,7 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-fx-oracle: { condition: service_started } + dev-fx-oracle: { condition: service_healthy } volumes: - ./api/billing/fees:/src/api/billing/fees - ./api/billing/fees/config.dev.yml:/app/config.yml:ro @@ -325,6 +337,12 @@ services: NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 FEES_GRPC_PORT: 50060 FEES_METRICS_PORT: 9402 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9402/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Billing Documents Service @@ -362,6 +380,12 @@ services: DOCUMENTS_MONGO_REPLICA_SET: dev-rs DOCUMENTS_GRPC_PORT: 50061 DOCUMENTS_METRICS_PORT: 9409 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9409/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Ledger Service @@ -377,8 +401,8 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } - dev-billing-fees: { condition: service_started } + dev-discovery: { condition: service_healthy } + dev-billing-fees: { condition: service_healthy } volumes: - ./api/ledger:/src/api/ledger - ./api/ledger/config.dev.yml:/app/config.yml:ro @@ -408,6 +432,12 @@ services: NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 LEDGER_GRPC_PORT: 50052 LEDGER_METRICS_PORT: 9401 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9401/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # FX Ingestor Service @@ -444,6 +474,12 @@ services: NATS_PASSWORD: ${NATS_PASSWORD} NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 FX_INGESTOR_METRICS_PORT: 9102 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9102/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Payments Orchestrator Service @@ -459,8 +495,8 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-ledger: { condition: service_started } - dev-billing-fees: { condition: service_started } + dev-ledger: { condition: service_healthy } + dev-billing-fees: { condition: service_healthy } volumes: - ./api/payments/orchestrator:/src/api/payments/orchestrator - ./api/payments/storage:/src/api/payments/storage @@ -485,6 +521,12 @@ services: NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 PAYMENTS_GRPC_PORT: 50062 PAYMENTS_METRICS_PORT: 9403 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9403/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Payments Quotation Service @@ -500,10 +542,10 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } - dev-fx-oracle: { condition: service_started } - dev-billing-fees: { condition: service_started } - dev-chain-gateway: { condition: service_started } + dev-discovery: { condition: service_healthy } + dev-fx-oracle: { condition: service_healthy } + dev-billing-fees: { condition: service_healthy } + dev-chain-gateway: { condition: service_healthy } volumes: - ./api/payments/quotation:/src/api/payments/quotation - ./api/payments/storage:/src/api/payments/storage @@ -529,6 +571,12 @@ services: FEES_ADDRESS: dev-billing-fees:50060 ORACLE_ADDRESS: dev-fx-oracle:50051 CHAIN_GATEWAY_ADDRESS: dev-chain-gateway:50053 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9414/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Payments Methods Service @@ -544,7 +592,7 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } + dev-discovery: { condition: service_healthy } volumes: - ./api/payments/methods:/src/api/payments/methods - ./api/payments/storage:/src/api/payments/storage @@ -576,6 +624,12 @@ services: NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 PAYMENTS_METHODS_GRPC_PORT: 50066 PAYMENTS_METHODS_METRICS_PORT: 9416 + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9416/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Chain Gateway Vault Agent (sidecar for AppRole authentication) @@ -623,7 +677,7 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } + dev-discovery: { condition: service_healthy } dev-vault: { condition: service_healthy } dev-chain-gateway-vault-agent: { condition: service_healthy } volumes: @@ -654,6 +708,12 @@ services: VAULT_ADDR: ${VAULT_ADDR} VAULT_TOKEN_FILE: /run/vault/token CHAIN_GATEWAY_RPC_URL: ${CHAIN_GATEWAY_RPC_URL} + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9406/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # TRON Gateway Vault Agent (sidecar for AppRole authentication) @@ -701,7 +761,7 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } + dev-discovery: { condition: service_healthy } dev-vault: { condition: service_healthy } dev-tron-gateway-vault-agent: { condition: service_healthy } volumes: @@ -734,6 +794,12 @@ services: TRON_GATEWAY_RPC_URL: ${TRON_GATEWAY_RPC_URL:-} TRON_GATEWAY_GRPC_URL: ${TRON_GATEWAY_GRPC_URL:-} TRON_GATEWAY_GRPC_TOKEN: ${TRON_GATEWAY_GRPC_TOKEN:-} + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9408/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Aurora Gateway Service (simulated card payouts) @@ -748,7 +814,7 @@ services: restart: unless-stopped depends_on: dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } + dev-discovery: { condition: service_healthy } dev-vault: { condition: service_healthy } volumes: - ./api/gateway/aurora:/src/api/gateway/aurora @@ -777,6 +843,12 @@ services: AURORA_GATEWAY_METRICS_PORT: 9405 AURORA_GATEWAY_HTTP_PORT: 8084 VAULT_ADDR: ${VAULT_ADDR} + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9405/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # ChimeraSettle Gateway Service (simulated settlements) @@ -792,7 +864,7 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-discovery: { condition: service_started } + dev-discovery: { condition: service_healthy } dev-vault: { condition: service_healthy } volumes: - ./api/gateway/chsettle:/src/api/gateway/chsettle @@ -819,6 +891,12 @@ services: CHSETTLE_GATEWAY_GRPC_PORT: 50080 CHSETTLE_GATEWAY_METRICS_PORT: 9406 VAULT_ADDR: ${VAULT_ADDR} + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9406/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Notification Service @@ -854,6 +932,12 @@ services: MONGO_PASSWORD: ${MONGO_PASSWORD} MONGO_AUTH_SOURCE: admin MONGO_REPLICA_SET: dev-rs + healthcheck: + test: ["CMD-SHELL", "wget -q -O- \"http://127.0.0.1:8081$${API_ENDPOINT}/health\" | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Callbacks Vault Agent (sidecar for AppRole authentication) @@ -928,6 +1012,12 @@ services: CALLBACKS_METRICS_PORT: 9420 VAULT_ADDR: ${VAULT_ADDR} VAULT_TOKEN_FILE: /run/vault/token + healthcheck: + test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9420/health | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # BFF Vault Agent (sidecar for AppRole authentication) @@ -976,11 +1066,11 @@ services: depends_on: dev-mongo-init: { condition: service_completed_successfully } dev-nats: { condition: service_started } - dev-ledger: { condition: service_started } - dev-payments-orchestrator: { condition: service_started } - dev-payments-quotation: { condition: service_started } - dev-payments-methods: { condition: service_started } - dev-chain-gateway: { condition: service_started } + dev-ledger: { condition: service_healthy } + dev-payments-orchestrator: { condition: service_healthy } + dev-payments-quotation: { condition: service_healthy } + dev-payments-methods: { condition: service_healthy } + dev-chain-gateway: { condition: service_healthy } dev-bff-vault-agent: { condition: service_healthy } volumes: - ./api/edge/bff:/src/api/edge/bff @@ -1020,6 +1110,12 @@ services: API_ENDPOINT: /api/v1 VAULT_ADDR: ${VAULT_ADDR} VAULT_TOKEN_FILE: /run/vault/token + healthcheck: + test: ["CMD-SHELL", "wget -q -O- \"http://127.0.0.1:8080$${API_ENDPOINT}/health\" | grep -q '\"status\":\"ok\"'"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 40s # -------------------------------------------------------------------------- # Frontend (Flutter Web)