Skip to content

feat(sandbox): first real E2E payment — 16 bugs fixed across 7 services (STA-243)#248

Merged
Puneethkumarck merged 1 commit into
mainfrom
feature/STA-243-sandbox-e2e-fixes
Apr 10, 2026
Merged

feat(sandbox): first real E2E payment — 16 bugs fixed across 7 services (STA-243)#248
Puneethkumarck merged 1 commit into
mainfrom
feature/STA-243-sandbox-e2e-fixes

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 23, 2026

Copy link
Copy Markdown
Owner

Summary

First successful end-to-end stablecoin sandwich payment through all 7 services with real sandbox APIs.

Payment: $5.00 USD → £3.73 GBP (US → GB corridor)
Payment ID: 9c6f2646-cbfc-4dcf-8be7-a76e38c58c55
Blockchain TX: 0x99950cbe... on Base Sepolia

Real APIs Used

Provider Service Purpose
Persona S2 KYC verification
Frankfurter S6 Live FX rates (ECB)
Stripe S3 ACH fiat collection + webhook
Alchemy S4 Base Sepolia EVM RPC
Circle S5 USDC redemption
Modulr S5 GBP Faster Payments payout

16 Bugs Found and Fixed

  1. Stripe confirm=true removal (ACH needs customer confirmation)
  2. Modulr raw auth key (no Bearer prefix)
  3. Modulr SCAN destination support (FPS)
  4. Modulr SEPA_CREDIT_TRANSFER scheme name
  5. Modulr FPS null scheme (auto-determine)
  6. Modulr reference truncation (18 char limit)
  7. Circle amount rounding (max 2 decimals)
  8. DevCustodyAdapter BigDecimal rounding
  9. OffRampActivityImpl dynamic payment rail (GBP=FPS, EUR=SEPA)
  10. USD↔GBP corridor missing (V6 migration + RateRefreshJob)
  11. GlobalExceptionHandler stack trace logging
  12. FallbackAdaptersConfig @ConditionalOnMissingBean
  13. DB credentials mismatch (sp_user → dev)
  14. Temporal postgres12 driver + dynamic config
  15. Jaeger image version 1.76.0
  16. Sandbox YAML external-api logging

New Files

  • docker-compose.sandbox.yml — full 12-container E2E stack
  • V6__seed_usd_gbp_corridor.sql — USD→GBP liquidity pool
  • Makefile sandbox targets (sandbox-up/down/build/test/status/tunnel/logs)

Test plan

  • All unit tests pass (pre-push gate)
  • Real E2E payment completed through all 7 services
  • Blockchain TX verified on BaseScan

Closes #243

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full sandbox E2E environment and new make targets for building, running, tunneling, testing, and inspecting the sandbox
    • Sandbox .env template updated with additional provider keys and webhook secret rename
    • GBP currency pair added to seeded liquidity pools; off-ramp now resolves rails for GBP and EUR
  • Bug Fixes

    • Circle redemptions now truncate amounts to two decimals
    • Improved minor-unit handling for custody transfers and clearer custody error logs
    • Stripe payment-intent flow refined (confirm omitted)
  • Infrastructure

    • New sandbox Docker Compose file; Jaeger image upgraded and Temporal config adjusted
    • Sandbox DB credentials unified across services

@Puneethkumarck Puneethkumarck added test Test coverage sandbox Sandbox / testnet integration cross-cutting Affects all services labels Mar 23, 2026
@coderabbitai

coderabbitai Bot commented Mar 23, 2026

Copy link
Copy Markdown

Walkthrough

Adds sandbox E2E infrastructure and Makefile targets, seeds FX liquidity corridors, standardizes sandbox DB credentials, tightens adapter numeric/serialization behavior, refactors off-ramp payout derivation to currency-based resolvers, and updates tests and env/template for sandbox runtimes.

Changes

Cohort / File(s) Summary
Makefile & sandbox compose
Makefile, docker-compose.sandbox.yml, docker-compose.dev.yml
Added sandbox-specific Make targets (env-check, build/up/down/destroy/logs/tunnel/run/test/status), introduced docker-compose.sandbox.yml describing infra + 7 application services for sandbox E2E; tweaked Temporal env and upgraded Jaeger in dev compose.
Service sandbox profiles (DB creds)
api-gateway-iam/.../application-sandbox.yml, blockchain-custody/.../application-sandbox.yml, compliance-travel-rule/.../application-sandbox.yml, fiat-off-ramp/.../application-sandbox.yml, fiat-on-ramp/.../application-sandbox.yml, fx-liquidity-engine/.../application-sandbox.yml, ledger-accounting/.../application-sandbox.yml, merchant-onboarding/.../application-sandbox.yml, merchant-iam/.../application-sandbox.yml, payment-orchestrator/.../application-sandbox.yml
Uniformly changed sandbox datasource credentials from sp_user/sp_passdev/dev.
Sandbox env template
.env.sandbox.template
Renamed Stripe webhook var, added custody private key and Modulr secret variables required by sandbox compose.
Off-ramp payout derivation
payment-orchestrator/.../OffRampActivityImpl.java
Replaced hardcoded recipient/bank constants with currency-based resolver helpers and added sha256 helper; currently supports GBP and EUR and throws on unsupported currencies.
Custody amount handling
blockchain-custody/.../DevCustodyAdapter.java
Change in USDC minor-unit conversion: strip trailing zeros, require zero scale and use toBigInteger() path with explicit error for fractional-minor amounts (prevents silent rounding).
Circle payout precision & tests
fiat-off-ramp/.../CircleRedemptionAdapter.java, fiat-off-ramp/.../CircleRedemptionAdapterTest.java
Validate amount non-null/positive; serialize amounts as 2-decimal string with RoundingMode.DOWN; updated tests and added truncation test.
Stripe PSP change & tests
fiat-on-ramp/.../StripePspAdapter.java, fiat-on-ramp/.../StripePspAdapterTest.java
Removed confirm=true from payment intent form; updated corresponding test expectation.
FX rate job & migrations
fx-liquidity-engine/.../RateRefreshJob.java, fx-liquidity-engine/.../db/migration/V6__seed_usd_gbp_corridor.sql, fx-liquidity-engine/.../db/migration/V7__seed_gbp_usd_corridor.sql, fx-liquidity-engine/.../RateRefreshJobTest.java
RateRefreshJob now derives corridors from LiquidityPoolRepository (constructor injection) instead of static list; added V6/V7 seed migrations for USD↔GBP corridors; tests updated to mock repository results.
Fallback adapters registration
merchant-onboarding/.../FallbackAdaptersConfig.java
Added @ConditionalOnMissingBean to fallback adapter beans so mocks register only when no real bean exists.
Exception logging tweak
blockchain-custody/.../GlobalExceptionHandler.java
Changed custody-signing error log to include ex.getMessage() (instead of class simple name).

Sequence Diagram(s)

sequenceDiagram
participant Client
participant PaymentOrch as Payment-Orchestrator
participant Compliance
participant FX
participant OnRamp
participant Custody
participant OffRamp
participant Kafka
participant DB
participant ExternalAPIs as SandboxProviders

Client->>PaymentOrch: initiate payment (S1)
PaymentOrch->>Compliance: submit for KYC (S2)
Compliance-->>PaymentOrch: approval/rejection (S2 result)
PaymentOrch->>FX: request quote (S6)
FX-->>PaymentOrch: quote
PaymentOrch->>OnRamp: initiate fiat collection (S3)
OnRamp->>ExternalAPIs: call provider sandbox (Stripe/Modulr) (webhooks expected)
OnRamp-->>PaymentOrch: collection success
PaymentOrch->>Custody: create blockchain transfer (S4)
Custody->>ExternalAPIs: submit to testnet (Alchemy)
Custody-->>PaymentOrch: tx confirmed
PaymentOrch->>OffRamp: request redemption & payout (S5, S7)
OffRamp->>ExternalAPIs: call Circle / Modulr sandbox
ExternalAPIs-->>OffRamp: webhook/callbacks
PaymentOrch->>Kafka: emit events
PaymentOrch->>DB: persist ledger entries
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

infra, feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the primary change: first E2E payment through sandbox with multiple bug fixes across services and references issue STA-243.
Description check ✅ Passed Description is comprehensive with summary, real APIs documented, 16 bugs enumerated, new files listed, and test plan validated. All required template sections addressed.
Linked Issues check ✅ Passed All acceptance criteria from #243 met: E2E payment flow completed (USD→GBP), blockchain TX verified, real sandbox APIs exercised across all 6 providers, webhook integration demonstrated, and ledger balanced.
Out of Scope Changes check ✅ Passed All changes directly support the E2E sandbox objective. Infrastructure updates (docker-compose.sandbox.yml, Makefile targets, migrations), adapter fixes, credential standardization, and provider integrations are all in-scope for STA-243.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/STA-243-sandbox-e2e-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java`:
- Line 65: The current log call in GlobalExceptionHandler.handleCustodySigning()
logs the full stack trace (log.error("Custody signing error: {}",
ex.getMessage(), ex)); update it to avoid printing the exception object/stack to
logs—log only the exception class and a safe message (e.g., include
ex.getClass().getSimpleName() and ex.getMessage() or just ex.getMessage()) so no
stack or sensitive crypto details are emitted; remove the throwable parameter
from the log call and ensure no other code in handleCustodySigning() logs the
exception object.

In
`@blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java`:
- Around line 105-108: DevCustodyAdapter currently truncates fractional minor
units by using setScale(0, RoundingMode.DOWN) when computing amountMinorUnits,
which diverges from FireblocksCustodyAdapter and can silently drop precision;
change the logic in DevCustodyAdapter around amountMinorUnits/request.amount()
to first validate the BigDecimal scale does not exceed USDC_DECIMALS (e.g., if
request.amount().scale() > USDC_DECIMALS throw an IllegalArgumentException with
a clear message), and then convert using movePointRight(USDC_DECIMALS) followed
by toBigIntegerExact() (or equivalent exact conversion) so any unexpected extra
precision fails fast instead of being truncated.

In `@docker-compose.sandbox.yml`:
- Line 139: Change the APP_EXTERNAL_API_LOGGING_ENABLED environment variable
default from "true" to "false" in docker-compose.sandbox.yml to prevent external
API payloads (PII/secrets) from being logged by default; locate every occurrence
of the APP_EXTERNAL_API_LOGGING_ENABLED key (there are multiple entries in this
file) and update their values to "false", keeping the variable name intact so it
can still be enabled manually for targeted debugging.
- Around line 224-225: Remove the hard-coded Stripe webhook secret from
docker-compose.sandbox.yml (the APP_PSP_STRIPE_WEBHOOK_WEBHOOK_SECRET entry),
rotate the leaked secret in Stripe immediately, and place the new secret into
.env.sandbox (or your secrets manager) instead; update
docker-compose.sandbox.yml to read APP_PSP_STRIPE_WEBHOOK_WEBHOOK_SECRET from
the environment (no literal value), commit the config change, and ensure you do
not re-commit the rotated secret or any .env file containing live credentials.
- Around line 363-367: Replace the fragile startup condition that uses
condition: service_started for the services compliance-travel-rule and
fx-liquidity-engine with a readiness-based check (use condition:
service_healthy) and ensure each target service defines a proper healthcheck; in
other words, update the depends_on/condition entries referencing
compliance-travel-rule and fx-liquidity-engine to service_healthy and add/verify
corresponding healthcheck configurations for those services so the orchestrator
waits for actual health readiness instead of process start.
- Around line 260-261: Remove the hardcoded private key value for
APP_CUSTODY_DEV_EVM_PRIVATE_KEY from docker-compose.sandbox.yml, replace it with
an environment variable reference (so the compose file reads the variable rather
than embedding the key), move the actual secret into .env.sandbox (create/update
.env.sandbox with APP_CUSTODY_DEV_EVM_PRIVATE_KEY=<new_key>), ensure
.env.sandbox is added to .gitignore and not committed, rotate the compromised
key by generating a new sandbox wallet and funding it, and treat the old key as
compromised/invalidated in any relevant configs or key stores.

In
`@fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java`:
- Around line 67-72: In CircleRedemptionAdapter where the CirclePayoutRequest is
constructed (the block using request.payoutId() and request.amount()), add an
explicit precondition check that request.amount() is non-null and greater than
zero before normalization/scale; if the check fails, throw a clear
IllegalArgumentException (or a domain-specific exception) with a descriptive
message (e.g., "amount must be provided and > 0") so you avoid NPEs and invalid
payout requests when building CirclePayoutRequest.CircleAmount.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java`:
- Around line 53-55: Update the test in CircleRedemptionAdapterTest to exercise
the normalization/rounding path by adding a case where RedemptionRequest
(constructed in aRedemptionRequest()) uses a value with more than 2 decimal
places (e.g., new BigDecimal("10000.009")); ensure the adapter
serializes/forwards the amount rounded/truncated to "10000.00" by asserting the
outbound JSON contains the string "10000.00" (or equivalent field value) so the
>2-decimal normalization is actually validated.

In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java`:
- Around line 23-27: SUPPORTED_CORRIDORS is a hard-coded list in RateRefreshJob
that duplicates corridor data kept in DB migrations; replace this static list
with a single authoritative source by injecting a domain port or repository
(e.g., CorridorRepository or CorridorPort) into RateRefreshJob and use its
method (e.g., findAllSupportedCorridors or loadRefreshCorridors) to fetch
Corridor instances at runtime; remove the SUPPORTED_CORRIDORS constant, update
the RateRefreshJob constructor to accept the repository, call the repository
method where the job currently iterates SUPPORTED_CORRIDORS, and add appropriate
null/empty handling and unit/integration test updates to reflect the new
dependency.

In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql`:
- Around line 9-13: Add a second INSERT row to the V6 migration that seeds the
reverse corridor (from_currency='GBP', to_currency='USD') so directional pool
lookups succeed when RateRefreshJob triggers both directions; mirror the
existing VALUES(...) entry (use gen_random_uuid() and the same liquidity
bounds/amounts appropriate for the reverse corridor) and keep the ON CONFLICT
(from_currency, to_currency) DO NOTHING clause so it is idempotent.

In `@Makefile`:
- Around line 241-243: The sandbox-status Makefile target is using the wrong
compose variable ($(COMPOSE)) and thus inspects the dev compose file; update the
sandbox-status recipe to use $(SANDBOX_COMPOSE) instead of $(COMPOSE) so it runs
the sandbox stack (keep the same ps flags and fallback behavior). Specifically
edit the sandbox-status target to call @$(SANDBOX_COMPOSE) ps --format "table
{{.Name}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || $(SANDBOX_COMPOSE) ps,
preserving the echo "" line.

In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`:
- Line 47: The code builds recipientAccountHash by concatenating "sha256:" with
request.recipientId(), but that's not a cryptographic hash; in
OffRampActivityImpl replace that concatenation with an actual SHA-256 digest of
the account details (e.g., request.recipientId() or the full account string you
intend to anonymize), using java.security.MessageDigest to compute the hash
bytes and encode them as hex (or base64) and then prefix with "sha256:". Ensure
you update the variable/field named recipientAccountHash and any tests to expect
the real hashed value rather than the plain concatenation.
- Around line 80-98: The current resolver helpers (resolvePaymentRail,
resolveBankAccount, resolveBankCode, resolveAccountType, resolveCountry)
silently default all non-"GBP" currencies to SEPA/DE values, which is unsafe;
update each resolver to validate targetCurrency explicitly and throw an
IllegalArgumentException for unsupported currencies (or at minimum log a
warning) instead of falling back to DE values—locate and modify the five static
methods named above to check for "GBP" and explicit supported currencies and
raise IllegalArgumentException with a clear message like "Unsupported
targetCurrency: <value>" when not supported.
- Around line 84-90: The code currently hardcodes bank routing in
OffRampActivityImpl via resolveBankAccount and resolveBankCode, which ignores
the OffRampRequest.recipientId; update the flow to obtain real recipient banking
details instead of using currency-only defaults: either extend OffRampRequest to
include recipient account and sort code/iban and use those fields in
OffRampActivityImpl when building payouts, or (preferred) inject and call a
RecipientRepository/RecipientService from OffRampActivityImpl to lookup banking
details by recipientId and use those returned values in place of
resolveBankAccount/resolveBankCode; remove or deprecate the hardcoded
resolveBankAccount/resolveBankCode usage and add null/validation checks and
error handling when recipient banking info is missing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e29416bb-b045-41fc-bbbf-629b88ec3f1c

📥 Commits

Reviewing files that changed from the base of the PR and between b193e88 and 2b4ed1d.

📒 Files selected for processing (23)
  • Makefile
  • api-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.yml
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java
  • blockchain-custody/blockchain-custody/src/main/resources/application-sandbox.yml
  • compliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.yml
  • docker-compose.dev.yml
  • docker-compose.sandbox.yml
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java
  • fiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.yml
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java
  • fiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.yml
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java
  • fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java
  • fx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.yml
  • fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql
  • ledger-accounting/ledger-accounting/src/main/resources/application-sandbox.yml
  • merchant-iam/merchant-iam/src/main/resources/application-sandbox.yml
  • merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java
  • merchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.yml
  • payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java
  • payment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml
💤 Files with no reviewable changes (2)
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java

Comment thread docker-compose.sandbox.yml Outdated
Comment thread docker-compose.sandbox.yml Outdated
Comment thread docker-compose.sandbox.yml Outdated
Comment on lines +9 to +13
VALUES (gen_random_uuid(), 'USD', 'GBP',
1000000.00000000, 0.00000000,
100000.00000000, 5000000.00000000,
now(), 0)
ON CONFLICT (from_currency, to_currency) DO NOTHING;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Seed the reverse GBP->USD pool as well.

Line 9 seeds only USD->GBP, but RateRefreshJob enables both directions (Line 26 and Line 27 in RateRefreshJob.java). Directional pool lookups can therefore fail for GBP->USD even though refresh attempts it.

🩹 Proposed migration update
 INSERT INTO liquidity_pools (pool_id, from_currency, to_currency,
                              available_balance, reserved_balance,
                              minimum_threshold, maximum_capacity,
                              updated_at, version)
-VALUES (gen_random_uuid(), 'USD', 'GBP',
-        1000000.00000000, 0.00000000,
-        100000.00000000, 5000000.00000000,
-        now(), 0)
+VALUES (gen_random_uuid(), 'USD', 'GBP',
+        1000000.00000000, 0.00000000,
+        100000.00000000, 5000000.00000000,
+        now(), 0),
+       (gen_random_uuid(), 'GBP', 'USD',
+        1000000.00000000, 0.00000000,
+        100000.00000000, 5000000.00000000,
+        now(), 0)
 ON CONFLICT (from_currency, to_currency) DO NOTHING;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql`
around lines 9 - 13, Add a second INSERT row to the V6 migration that seeds the
reverse corridor (from_currency='GBP', to_currency='USD') so directional pool
lookups succeed when RateRefreshJob triggers both directions; mirror the
existing VALUES(...) entry (use gen_random_uuid() and the same liquidity
bounds/amounts appropriate for the reverse corridor) and keep the ON CONFLICT
(from_currency, to_currency) DO NOTHING clause so it is idempotent.

Comment thread Makefile
Sandbox E2E: $5 USD → £3.73 GBP through all 7 services with real APIs
(Persona, Frankfurter, Stripe, Alchemy, Circle, Modulr).

16 bugs found/fixed during E2E + PR review remediation:
- Externalize secrets to .env.sandbox (Stripe webhook, EVM key)
- Default external API logging to false (PII protection)
- Remove crypto stack trace from CustodySigningException handler
- DevCustodyAdapter: fail-fast on sub-minor-unit precision
- CircleRedemptionAdapter: validate amount is non-null and positive
- OffRampActivityImpl: explicit currency switch (no silent SEPA fallback)
- OffRampActivityImpl: real SHA-256 hash for recipientAccountHash
- RateRefreshJob: load corridors from LiquidityPoolRepository (no hardcoded list)
- V7 migration: seed reverse GBP→USD liquidity pool
- Orchestrator depends_on: service_healthy for compliance + fx
- Makefile: fix sandbox-status to use SANDBOX_COMPOSE
- Circle rounding edge-case test (>2 decimals)

Closes #243

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck force-pushed the feature/STA-243-sandbox-e2e-fixes branch from 2b4ed1d to 0531f42 Compare March 23, 2026 04:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.sandbox.template:
- Around line 21-24: The env var name in the sandbox template doesn't match the
YAML binding: update the variable in .env.sandbox.template from
CUSTODY_DEV_EVM_PRIVATE_KEY to DEV_CUSTODY_EVM_PRIVATE_KEY so it matches the
binding referenced in blockchain-custody/application.yml
(${DEV_CUSTODY_EVM_PRIVATE_KEY:}); ensure the value is a hex private key (no 0x
prefix) and keep the same comment context so the custody adapter (sandbox
signing) receives the expected key.

In `@docker-compose.sandbox.yml`:
- Around line 19-20: The published ports in docker-compose.sandbox.yml are
currently exposed on all interfaces (e.g., the mapping "5432:5432"); update
these mappings to bind to loopback by prefixing the host side with 127.0.0.1
(e.g., "127.0.0.1:5432:5432") for sandbox runs where APP_SECURITY_ENABLED is
false, and apply the same change to all other published-port entries referenced
in the comment (the mappings at the other locations). Ensure the change is
applied consistently to the port strings used by the affected services so ports
are only reachable from localhost when APP_SECURITY_ENABLED is not enabled.
- Line 39: Replace all Docker image entries that use mutable tags (e.g.,
timescale/timescaledb:latest-pg17 and the other image lines flagged) with pinned
immutable references — either explicit version tags (for example
timescale/timescaledb:<exact-version>) or content-addressable digests (e.g.,
timescale/timescaledb@sha256:<digest>). Update each image line in the compose
file to use the chosen immutable tag/digest so sandbox E2E runs are
deterministic; ensure you pin every occurrence of the mutable images reported
(all image entries currently using :latest or similar mutable tags).

In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java`:
- Around line 29-31: In RateRefreshJob.refreshRates, avoid letting a bad
liquidity pool abort the full corridor materialization: replace the direct
stream map that calls new Corridor(pool.fromCurrency(), pool.toCurrency()) with
defensive filtering/validation and per-pool error handling—e.g., filter out
pools with blank currencies or identical from/to, and wrap construction in a
try/catch to log and skip malformed rows returned by
liquidityPoolRepository.findAll(); ensure the resulting corridors list only
contains successfully constructed Corridor instances so the refresh can proceed
even if some rows are invalid.

In `@Makefile`:
- Around line 7-9: The .PHONY list is missing several sandbox targets so make
might skip their recipes when files with those names exist; update the .PHONY
declaration to include the sandbox targets: add sandbox-build, sandbox-destroy,
sandbox-logs, and the pattern sandbox-test-% (in addition to existing
sandbox-run-%) so the targets defined later (sandbox-build, sandbox-destroy,
sandbox-logs, sandbox-test-%) are always executed as phony targets.
- Around line 186-190: Update the sandbox-env-check Makefile target to validate
all required sandbox env vars by adding the same pattern used for
STRIPE_TEST_SECRET_KEY and ALCHEMY_API_KEY for each missing secret/ID: e.g., add
checks for CIRCLE_DESTINATION, MODULR_CLIENT_ID, MODULR_CLIENT_SECRET,
PERSONA_API_KEY, STRIPE_WEBHOOK_SECRET, CUSTODY_PRIVATE_KEY (or whatever exact
names are used in the codebase) using ". ./.env.sandbox && test -n
\"$$VAR_NAME\" || (echo \"ERROR: VAR_NAME not set in .env.sandbox\" && exit 1)";
keep the same overall structure and final success echo in the sandbox-env-check
target.

In
`@merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java`:
- Around line 33-53: The real adapters (OnfidoKybAdapter and
CompaniesHouseAdapter) are being registered alongside fallback beans because
they lack the fallback toggle; add a guard identical to
TemporalOnboardingWorkflowAdapter by annotating the configurations/beans for
OnfidoKybAdapter and CompaniesHouseAdapter with `@ConditionalOnProperty`(name =
"app.fallback-adapters.enabled", havingValue = "false", matchIfMissing = true)
so that when app.fallback-adapters.enabled=true the real adapters are not
created and the mocks in FallbackAdaptersConfig are used exclusively.

In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`:
- Around line 93-107: The hardcoded sandbox bank details in resolveBankAccount
and resolveBankCode (values like "12345678", "000000", "DE89370400440532013000")
must be documented or externalized; either add an explicit comment above both
methods stating these are Modulr sandbox test fixtures and not production data,
or refactor these methods to read mappings from configuration (e.g., a
properties/Env/config client) keyed by currency and throw the same
IllegalArgumentException for unknown currencies; update any callers of
resolveBankAccount/resolveBankCode to use the new config-backed lookup and add a
small unit test or fixture showing the sandbox config values are used in tests.
- Around line 85-123: The five resolver methods (resolvePaymentRail,
resolveBankAccount, resolveBankCode, resolveAccountType, resolveCountry) all use
the same currency switch logic; consolidate by introducing a single immutable
currency config mapping (e.g., a Map<String, CurrencyConfig> or a CurrencyConfig
record) keyed by currency code, populate it with entries for "GBP" and "EUR",
and replace the individual switch-based methods with lookups that retrieve the
CurrencyConfig for the given targetCurrency and return the appropriate field
(throwing the same IllegalArgumentException if the key is missing); update
callers to use the new lookup methods or accessors so behavior remains identical
but maintenance is centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7f332daf-17a7-4cf9-99ab-b543f69ae86b

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4ed1d and 0531f42.

📒 Files selected for processing (26)
  • .env.sandbox.template
  • Makefile
  • api-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.yml
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java
  • blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java
  • blockchain-custody/blockchain-custody/src/main/resources/application-sandbox.yml
  • compliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.yml
  • docker-compose.dev.yml
  • docker-compose.sandbox.yml
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java
  • fiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.yml
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java
  • fiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.yml
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java
  • fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java
  • fx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.yml
  • fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql
  • fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V7__seed_gbp_usd_corridor.sql
  • fx-liquidity-engine/fx-liquidity-engine/src/test/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJobTest.java
  • ledger-accounting/ledger-accounting/src/main/resources/application-sandbox.yml
  • merchant-iam/merchant-iam/src/main/resources/application-sandbox.yml
  • merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java
  • merchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.yml
  • payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java
  • payment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml
💤 Files with no reviewable changes (2)
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java

Comment thread .env.sandbox.template
Comment on lines +21 to +24
# --- S4 Blockchain & Custody (Dev EVM Wallet) ---
# Generate a new Sepolia testnet wallet private key (no 0x prefix)
# Fund it via https://www.alchemy.com/faucets/base-sepolia
CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Environment variable mismatch — custody private key won't bind.

CUSTODY_DEV_EVM_PRIVATE_KEY is defined here, but blockchain-custody/application.yml:125 binds to ${DEV_CUSTODY_EVM_PRIVATE_KEY:}. The custody adapter will receive an empty key and fail during sandbox signing.

Align the naming — recommend using the existing DEV_CUSTODY_EVM_PRIVATE_KEY in the template to match the YAML binding.

Proposed fix
 # --- S4 Blockchain & Custody (Dev EVM Wallet) ---
 # Generate a new Sepolia testnet wallet private key (no 0x prefix)
 # Fund it via https://www.alchemy.com/faucets/base-sepolia
-CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here
+DEV_CUSTODY_EVM_PRIVATE_KEY=your_evm_private_key_hex_here
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# --- S4 Blockchain & Custody (Dev EVM Wallet) ---
# Generate a new Sepolia testnet wallet private key (no 0x prefix)
# Fund it via https://www.alchemy.com/faucets/base-sepolia
CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here
# --- S4 Blockchain & Custody (Dev EVM Wallet) ---
# Generate a new Sepolia testnet wallet private key (no 0x prefix)
# Fund it via https://www.alchemy.com/faucets/base-sepolia
DEV_CUSTODY_EVM_PRIVATE_KEY=your_evm_private_key_hex_here
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.sandbox.template around lines 21 - 24, The env var name in the sandbox
template doesn't match the YAML binding: update the variable in
.env.sandbox.template from CUSTODY_DEV_EVM_PRIVATE_KEY to
DEV_CUSTODY_EVM_PRIVATE_KEY so it matches the binding referenced in
blockchain-custody/application.yml (${DEV_CUSTODY_EVM_PRIVATE_KEY:}); ensure the
value is a hex private key (no 0x prefix) and keep the same comment context so
the custody adapter (sandbox signing) receives the expected key.

Comment on lines +19 to +20
ports:
- "5432:5432"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restrict published ports to localhost when security is disabled.

At Line 20 and other published-port lines, services are exposed on all interfaces while APP_SECURITY_ENABLED is false. For sandbox runs with real provider credentials/data, bind ports to loopback (127.0.0.1) to reduce unintended network exposure.

Proposed hardening pattern
-      - "8085:8085"
+      - "127.0.0.1:8085:8085"

Apply the same pattern to all published service/data-store ports unless remote access is explicitly required.

Also applies to: 42-43, 61-62, 85-86, 100-101, 127-128, 167-168, 204-205, 242-243, 282-283, 323-324, 355-356

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.sandbox.yml` around lines 19 - 20, The published ports in
docker-compose.sandbox.yml are currently exposed on all interfaces (e.g., the
mapping "5432:5432"); update these mappings to bind to loopback by prefixing the
host side with 127.0.0.1 (e.g., "127.0.0.1:5432:5432") for sandbox runs where
APP_SECURITY_ENABLED is false, and apply the same change to all other
published-port entries referenced in the comment (the mappings at the other
locations). Ensure the change is applied consistently to the port strings used
by the affected services so ports are only reachable from localhost when
APP_SECURITY_ENABLED is not enabled.

retries: 10

timescaledb:
image: timescale/timescaledb:latest-pg17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify mutable tags in sandbox compose
rg -n '^\s*image:\s+\S+:latest(\S*)?$' docker-compose.sandbox.yml

Repository: Puneethkumarck/stablebridge-platform

Length of output: 543


Pin Docker images to immutable versions/digests for deterministic E2E sandbox runs.

Mutable tags (:latest, :latest-pg17) introduce non-determinism. Silently changing behavior between runs breaks test reliability and makes regression detection impossible.

Affected lines: 39, 97, 124, 164, 201, 239, 279, 320, 352

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.sandbox.yml` at line 39, Replace all Docker image entries that
use mutable tags (e.g., timescale/timescaledb:latest-pg17 and the other image
lines flagged) with pinned immutable references — either explicit version tags
(for example timescale/timescaledb:<exact-version>) or content-addressable
digests (e.g., timescale/timescaledb@sha256:<digest>). Update each image line in
the compose file to use the chosen immutable tag/digest so sandbox E2E runs are
deterministic; ensure you pin every occurrence of the mutable images reported
(all image entries currently using :latest or similar mutable tags).

Comment on lines +29 to +31
var corridors = liquidityPoolRepository.findAll().stream()
.map(pool -> new Corridor(pool.fromCurrency(), pool.toCurrency()))
.toList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden corridor materialization to prevent full refresh abort on bad pool rows.

If one liquidity_pools record is invalid (blank currency or same from/to), new Corridor(...) throws during stream mapping and exits refreshRates() before any valid corridor is processed.

🔧 Proposed defensive fix
+import com.stablecoin.payments.fx.domain.model.LiquidityPool;
+import java.util.Optional;
@@
-        var corridors = liquidityPoolRepository.findAll().stream()
-                .map(pool -> new Corridor(pool.fromCurrency(), pool.toCurrency()))
-                .toList();
+        var corridors = liquidityPoolRepository.findAll().stream()
+                .map(this::toCorridorSafely)
+                .flatMap(Optional::stream)
+                .distinct()
+                .toList();
@@
     public void refreshRates() {
@@
         log.debug("Rate refresh complete: {}/{} corridors updated", refreshed, corridors.size());
     }
+
+    private Optional<Corridor> toCorridorSafely(LiquidityPool pool) {
+        try {
+            return Optional.of(new Corridor(pool.fromCurrency(), pool.toCurrency()));
+        } catch (IllegalArgumentException ex) {
+            log.warn("Skipping invalid corridor {}->{}: {}", pool.fromCurrency(), pool.toCurrency(), ex.getMessage());
+            return Optional.empty();
+        }
+    }
 }

As per coding guidelines, "Rate caching must handle stale data correctly".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java`
around lines 29 - 31, In RateRefreshJob.refreshRates, avoid letting a bad
liquidity pool abort the full corridor materialization: replace the direct
stream map that calls new Corridor(pool.fromCurrency(), pool.toCurrency()) with
defensive filtering/validation and per-pool error handling—e.g., filter out
pools with blank currencies or identical from/to, and wrap construction in a
try/catch to log and skip malformed rows returned by
liquidityPoolRepository.findAll(); ensure the resulting corridors list only
contains successfully constructed Corridor instances so the refresh can proceed
even if some rows are invalid.

Comment thread Makefile
Comment on lines +7 to +9
e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \
sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test \
sandbox-tunnel sandbox-env-check

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add missing sandbox targets to .PHONY.

sandbox-build, sandbox-destroy, sandbox-logs, and sandbox-test-% are defined later but not declared phony. If matching files exist, make may skip those recipes.

Proposed fix
 .PHONY: help build clean test test-unit test-integration test-business \
        format lint check \
        infra-up infra-down infra-destroy infra-status infra-logs infra-logs-% \
        run-% db-reset db-psql topics \
        deps outdated \
        assemble sonar fresh ci \
        e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \
-       sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test \
-       sandbox-tunnel sandbox-env-check
+       sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test sandbox-test-% \
+       sandbox-build sandbox-destroy sandbox-logs sandbox-tunnel sandbox-env-check
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \
sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test \
sandbox-tunnel sandbox-env-check
e2e-up e2e-down e2e-destroy e2e-status e2e-build e2e-test e2e \
sandbox-up sandbox-down sandbox-status sandbox-run-% sandbox-test sandbox-test-% \
sandbox-build sandbox-destroy sandbox-logs sandbox-tunnel sandbox-env-check
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 7 - 9, The .PHONY list is missing several sandbox
targets so make might skip their recipes when files with those names exist;
update the .PHONY declaration to include the sandbox targets: add sandbox-build,
sandbox-destroy, sandbox-logs, and the pattern sandbox-test-% (in addition to
existing sandbox-run-%) so the targets defined later (sandbox-build,
sandbox-destroy, sandbox-logs, sandbox-test-%) are always executed as phony
targets.

Comment thread Makefile
Comment on lines +186 to +190
sandbox-env-check: ## Verify .env.sandbox exists and key vars are set
@test -f .env.sandbox || (echo "ERROR: .env.sandbox not found. Run: cp .env.sandbox.template .env.sandbox" && exit 1)
@. ./.env.sandbox && test -n "$$STRIPE_TEST_SECRET_KEY" || (echo "ERROR: STRIPE_TEST_SECRET_KEY not set in .env.sandbox" && exit 1)
@. ./.env.sandbox && test -n "$$ALCHEMY_API_KEY" || (echo "ERROR: ALCHEMY_API_KEY not set in .env.sandbox" && exit 1)
@echo "✓ .env.sandbox loaded — keys present"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Expand sandbox-env-check to validate all required sandbox secrets/IDs.

Current checks only cover Stripe API key and Alchemy key. The compose stack and sandbox tests also depend on additional required vars (e.g., Circle destination, Modulr credentials/secrets, Persona keys, Stripe webhook secret, custody private key). Missing values will fail late at runtime.

Proposed fix
 sandbox-env-check: ## Verify .env.sandbox exists and key vars are set
 	`@test` -f .env.sandbox || (echo "ERROR: .env.sandbox not found. Run: cp .env.sandbox.template .env.sandbox" && exit 1)
-	@. ./.env.sandbox && test -n "$$STRIPE_TEST_SECRET_KEY" || (echo "ERROR: STRIPE_TEST_SECRET_KEY not set in .env.sandbox" && exit 1)
-	@. ./.env.sandbox && test -n "$$ALCHEMY_API_KEY" || (echo "ERROR: ALCHEMY_API_KEY not set in .env.sandbox" && exit 1)
+	`@set` -a; . ./.env.sandbox; set +a; \
+	required="STRIPE_TEST_SECRET_KEY STRIPE_SANDBOX_WEBHOOK_SECRET ALCHEMY_API_KEY CUSTODY_DEV_EVM_PRIVATE_KEY PERSONA_SANDBOX_API_KEY PERSONA_INQUIRY_TEMPLATE_ID CIRCLE_SANDBOX_API_KEY CIRCLE_SANDBOX_DESTINATION_ID MODULR_SANDBOX_API_KEY MODULR_SANDBOX_SOURCE_ACCOUNT_ID MODULR_SANDBOX_API_SECRET"; \
+	for k in $$required; do eval "v=\$${$$k}"; [ -n "$$v" ] || { echo "ERROR: $$k not set in .env.sandbox"; exit 1; }; done
 	`@echo` "✓ .env.sandbox loaded — keys present"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 186 - 190, Update the sandbox-env-check Makefile
target to validate all required sandbox env vars by adding the same pattern used
for STRIPE_TEST_SECRET_KEY and ALCHEMY_API_KEY for each missing secret/ID: e.g.,
add checks for CIRCLE_DESTINATION, MODULR_CLIENT_ID, MODULR_CLIENT_SECRET,
PERSONA_API_KEY, STRIPE_WEBHOOK_SECRET, CUSTODY_PRIVATE_KEY (or whatever exact
names are used in the codebase) using ". ./.env.sandbox && test -n
\"$$VAR_NAME\" || (echo \"ERROR: VAR_NAME not set in .env.sandbox\" && exit 1)";
keep the same overall structure and final success echo in the sandbox-env-check
target.

Comment on lines +33 to 53
@ConditionalOnMissingBean
KybProvider mockKybProvider() {
return new MockKybAdapter();
}

@Bean
@ConditionalOnMissingBean
CompanyRegistryProvider mockCompanyRegistryProvider() {
return new MockCompanyRegistryAdapter();
}

@Bean
@ConditionalOnMissingBean
DocumentStore mockDocumentStore() {
return new MockDocumentStoreAdapter();
}

@Bean
@ConditionalOnMissingBean
OnboardingWorkflowPort mockOnboardingWorkflow() {
return new MockOnboardingWorkflowAdapter();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify conditional annotations and potential overlap between fallback and real adapters.
files=$(fd 'FallbackAdaptersConfig\.java|OnfidoKybAdapter\.java|CompaniesHouseAdapter\.java|TemporalOnboardingWorkflowAdapter\.java')

for f in $files; do
  echo "===== $f ====="
  rg -n -C2 '@ConditionalOnProperty|@ConditionalOnMissingBean|class .*Adapter|@Bean' "$f"
  echo
done

# Expected verification outcome:
# - FallbackAdaptersConfig beans are guarded by `@ConditionalOnMissingBean`.
# - OnfidoKybAdapter / CompaniesHouseAdapter are not additionally gated by app.fallback-adapters.enabled=false.
# If both are true, fallback mode can still select real adapters for those ports.

Repository: Puneethkumarck/stablebridge-platform

Length of output: 8532


Real adapters activate concurrently with fallback beans when provider properties are set, defeating fallback-mode intent.

When app.fallback-adapters.enabled=true and app.kyb.provider=onfido (or app.company-registry.provider=companies-house), both OnfidoKybAdapter and the mock fallback beans enter the context. Since @ConditionalOnMissingBean finds the real adapter already registered, the mock is never created—you silently route to production services in sandbox/test.

TemporalOnboardingWorkflowAdapter correctly uses @ConditionalOnProperty(name = "app.fallback-adapters.enabled", havingValue = "false", matchIfMissing = true) to enforce mutual exclusion. Apply the same pattern: gate OnfidoKybAdapter and CompaniesHouseAdapter with app.fallback-adapters.enabled=false to prevent this collision.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java`
around lines 33 - 53, The real adapters (OnfidoKybAdapter and
CompaniesHouseAdapter) are being registered alongside fallback beans because
they lack the fallback toggle; add a guard identical to
TemporalOnboardingWorkflowAdapter by annotating the configurations/beans for
OnfidoKybAdapter and CompaniesHouseAdapter with `@ConditionalOnProperty`(name =
"app.fallback-adapters.enabled", havingValue = "false", matchIfMissing = true)
so that when app.fallback-adapters.enabled=true the real adapters are not
created and the mocks in FallbackAdaptersConfig are used exclusively.

Comment on lines +85 to +123
private static String resolvePaymentRail(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "FASTER_PAYMENTS";
case "EUR" -> "SEPA";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

private static String resolveBankAccount(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "12345678";
case "EUR" -> "DE89370400440532013000";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

private static String resolveBankCode(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "000000";
case "EUR" -> "COBADEFFXXX";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

private static String resolveAccountType(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "SORT_CODE";
case "EUR" -> "IBAN";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

private static String resolveCountry(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "GB";
case "EUR" -> "DE";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Explicit currency validation is correct; consider consolidating the repeated switch logic.

The IllegalArgumentException for unsupported currencies addresses the prior silent-fallback concern. However, five nearly identical switch expressions introduce maintenance overhead.

♻️ Optional: Consolidate into a single currency config record
+    private record CurrencyConfig(String rail, String account, String code, String accountType, String country) {}
+
+    private static final Map<String, CurrencyConfig> CURRENCY_CONFIGS = Map.of(
+        "GBP", new CurrencyConfig("FASTER_PAYMENTS", "12345678", "000000", "SORT_CODE", "GB"),
+        "EUR", new CurrencyConfig("SEPA", "DE89370400440532013000", "COBADEFFXXX", "IBAN", "DE")
+    );
+
+    private static CurrencyConfig requireConfig(String currency) {
+        var cfg = CURRENCY_CONFIGS.get(currency);
+        if (cfg == null) throw new IllegalArgumentException("Unsupported payout currency: " + currency);
+        return cfg;
+    }

Then replace each resolver with a single map lookup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`
around lines 85 - 123, The five resolver methods (resolvePaymentRail,
resolveBankAccount, resolveBankCode, resolveAccountType, resolveCountry) all use
the same currency switch logic; consolidate by introducing a single immutable
currency config mapping (e.g., a Map<String, CurrencyConfig> or a CurrencyConfig
record) keyed by currency code, populate it with entries for "GBP" and "EUR",
and replace the individual switch-based methods with lookups that retrieve the
CurrencyConfig for the given targetCurrency and return the appropriate field
(throwing the same IllegalArgumentException if the key is missing); update
callers to use the new lookup methods or accessors so behavior remains identical
but maintenance is centralized.

Comment on lines +93 to +107
private static String resolveBankAccount(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "12345678";
case "EUR" -> "DE89370400440532013000";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

private static String resolveBankCode(String targetCurrency) {
return switch (targetCurrency) {
case "GBP" -> "000000";
case "EUR" -> "COBADEFFXXX";
default -> throw new IllegalArgumentException("Unsupported payout currency: " + targetCurrency);
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Hardcoded bank details are sandbox test fixtures — document or extract.

These values (12345678, 000000, DE89370400440532013000) are Modulr sandbox test accounts per PR objectives. Add a comment clarifying this, or extract to configuration for easier discovery and future corridor expansion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java`
around lines 93 - 107, The hardcoded sandbox bank details in resolveBankAccount
and resolveBankCode (values like "12345678", "000000", "DE89370400440532013000")
must be documented or externalized; either add an explicit comment above both
methods stating these are Modulr sandbox test fixtures and not production data,
or refactor these methods to read mappings from configuration (e.g., a
properties/Env/config client) keyed by currency and throw the same
IllegalArgumentException for unknown currencies; update any callers of
resolveBankAccount/resolveBankCode to use the new config-backed lookup and add a
small unit test or fixture showing the sandbox config values are used in tests.

@github-actions

github-actions Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

This pull request has been inactive for 14 days and has been marked as stale. Please rebase, resolve any conflicts, and update the PR if it is still intended to be merged. It will be automatically closed in 7 days if there is no further activity.

@github-actions github-actions Bot added the stale label Apr 7, 2026
@Puneethkumarck Puneethkumarck merged commit 6d6bc7c into main Apr 10, 2026
36 of 39 checks passed
@Puneethkumarck Puneethkumarck deleted the feature/STA-243-sandbox-e2e-fixes branch April 10, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cross-cutting Affects all services sandbox Sandbox / testnet integration stale test Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sandbox): full sandwich E2E test against real sandbox APIs (STA-214)

1 participant