diff --git a/.env.sandbox.template b/.env.sandbox.template index 677dcb77..88fcb521 100644 --- a/.env.sandbox.template +++ b/.env.sandbox.template @@ -11,13 +11,18 @@ # Signup: https://dashboard.stripe.com/register # Get test key from: Dashboard > Developers > API Keys (toggle "Test mode") STRIPE_TEST_SECRET_KEY=sk_test_your_key_here -STRIPE_TEST_WEBHOOK_SECRET=whsec_your_webhook_secret_here +STRIPE_SANDBOX_WEBHOOK_SECRET=whsec_your_webhook_secret_here # --- S4 Blockchain & Custody (Alchemy) --- # Signup: https://auth.alchemy.com/signup # Create app for Base Sepolia, copy API key ALCHEMY_API_KEY=your_alchemy_api_key_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 +CUSTODY_DEV_EVM_PRIVATE_KEY=your_evm_private_key_hex_here + # --- S4 Blockchain & Custody (Fireblocks — optional) --- # Request sandbox: https://www.fireblocks.com/developer-sandbox-sign-up/ # Download RSA private key PEM file from Fireblocks console @@ -52,6 +57,7 @@ CIRCLE_SANDBOX_DESTINATION_ID=your_wire_destination_id # Request sandbox: https://www.modulrfinance.com/sandbox MODULR_SANDBOX_API_KEY=your_modulr_key_here MODULR_SANDBOX_SOURCE_ACCOUNT_ID=your_source_account_id +MODULR_SANDBOX_API_SECRET=your_modulr_hmac_secret_here # --- S10 + S13 (JWT Signing Key) --- # Generate ES256 key pair: diff --git a/Makefile b/Makefile index d6ca8073..dc50616f 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,9 @@ 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 + 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 # ───────────────────────────────────────────── # Variables @@ -170,3 +172,83 @@ e2e-test: ## Run Phase 3 E2E tests (stack must be running) PHASE3_TESTS_ENABLED=true $(GRADLE) :phase3-integration-tests:test --rerun e2e: e2e-build e2e-up e2e-test ## Build images, start stack, run E2E tests + +# ───────────────────────────────────────────── +# Sandbox Testing (real external APIs) +# ───────────────────────────────────────────── +# Requires: .env.sandbox with real API keys (copy from .env.sandbox.template) +# Infra: Docker Compose for Postgres, Kafka, Redis, Temporal +# Tunnel: cloudflared for Stripe webhooks + +SANDBOX_SERVICES := compliance-travel-rule fx-liquidity-engine fiat-on-ramp \ + blockchain-custody fiat-off-ramp ledger-accounting payment-orchestrator + +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" + +SANDBOX_COMPOSE := docker compose --env-file .env.sandbox -f docker-compose.sandbox.yml + +sandbox-up: sandbox-env-check sandbox-build ## Build images, start infra + all 7 services with real sandbox APIs + $(SANDBOX_COMPOSE) up -d + @echo "" + @echo "✓ Sandbox stack launching (7 services + infra)" + @echo " Run: make sandbox-status to check health" + @echo " Run: make sandbox-logs to tail all logs" + @echo " Run: make sandbox-test to run adapter tests" + +sandbox-down: ## Stop all sandbox containers + $(SANDBOX_COMPOSE) down + @echo "✓ Sandbox stopped" + +sandbox-destroy: ## Stop sandbox and remove volumes (full reset) + $(SANDBOX_COMPOSE) down -v + @echo "✓ Sandbox destroyed" + +sandbox-build: ## Build all service Docker images for sandbox + $(GRADLE) $(foreach s,$(SERVICES),:$(s):$(s):jibDockerBuild) --parallel + @echo "✓ All Docker images built" + +sandbox-logs: ## Tail all sandbox service logs + $(SANDBOX_COMPOSE) logs -f --tail=50 + +sandbox-tunnel: ## Start cloudflared tunnel for Stripe webhooks (runs in foreground) + @echo "Starting Cloudflare tunnel → localhost:8085 (S3 Fiat On-Ramp)" + @echo "Copy the https://xxx.trycloudflare.com URL to Stripe webhook dashboard" + @echo "Endpoint path: /on-ramp/internal/webhooks/psp/stripe" + @echo "" + cloudflared tunnel --url http://localhost:8085 + +sandbox-run-%: sandbox-env-check ## Run a service in sandbox mode (e.g., make sandbox-run-fiat-on-ramp) + set -a && . ./.env.sandbox && set +a && \ + $(GRADLE) :$*:$*:bootRun --args='--spring.profiles.active=sandbox' + +sandbox-test: sandbox-env-check ## Run all sandbox adapter tests against real APIs + set -a && . ./.env.sandbox && set +a && \ + $(GRADLE) \ + :fiat-on-ramp:fiat-on-ramp:test --tests '*StripeAdapterSandboxTest*' \ + :blockchain-custody:blockchain-custody:test --tests '*EvmRpcAdapterSandboxTest*' --tests '*SolanaRpcAdapterSandboxTest*' --tests '*FireblocksCustodyAdapterSandboxTest*' \ + :fiat-off-ramp:fiat-off-ramp:test --tests '*CircleRedemptionAdapterSandboxTest*' --tests '*ModulrPayoutAdapterSandboxTest*' \ + :fx-liquidity-engine:fx-liquidity-engine:test --tests '*FrankfurterRateAdapterSandboxTest*' \ + :merchant-onboarding:merchant-onboarding:test --tests '*CompaniesHouseAdapterSandboxTest*' + +sandbox-test-%: sandbox-env-check ## Run sandbox tests for a service (e.g., make sandbox-test-fiat-on-ramp) + set -a && . ./.env.sandbox && set +a && \ + $(GRADLE) :$*:$*:test --tests '*SandboxTest*' + +sandbox-status: ## Show infra status + sandbox env summary + @$(SANDBOX_COMPOSE) ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || $(SANDBOX_COMPOSE) ps + @echo "" + @echo "--- Sandbox API Keys ---" + @test -f .env.sandbox && . ./.env.sandbox && \ + echo "Stripe: $${STRIPE_TEST_SECRET_KEY:+set}" && \ + echo "Alchemy: $${ALCHEMY_API_KEY:+set}" && \ + echo "Persona: $${PERSONA_SANDBOX_API_KEY:+set}" && \ + echo "Companies House: $${COMPANIES_HOUSE_API_KEY:+set}" && \ + echo "Circle: $${CIRCLE_SANDBOX_API_KEY:+set}" && \ + echo "Modulr: $${MODULR_SANDBOX_API_KEY:+set}" && \ + echo "Fireblocks: $${FIREBLOCKS_SANDBOX_API_KEY:+set}" && \ + echo "JWT Key: $${JWT_PRIVATE_KEY_BASE64:+set}" \ + || echo "No .env.sandbox found" diff --git a/api-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.yml b/api-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.yml index e339c884..a99463a5 100644 --- a/api-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.yml +++ b/api-gateway-iam/api-gateway-iam/src/main/resources/application-sandbox.yml @@ -27,8 +27,8 @@ api-gateway-iam: spring: datasource: url: jdbc:postgresql://localhost:5432/s10_api_gateway_iam - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java index b4a00538..886b5b9f 100644 --- a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/application/controller/GlobalExceptionHandler.java @@ -62,7 +62,7 @@ public ApiError handleWalletNotFound(WalletNotFoundException ex) { @ResponseStatus(INTERNAL_SERVER_ERROR) @ExceptionHandler(CustodySigningException.class) public ApiError handleCustodySigning(CustodySigningException ex) { - log.error("Custody signing error: {}", ex.getClass().getSimpleName()); + log.error("Custody signing error: {}", ex.getMessage()); return ApiError.of(CustodySigningException.ERROR_CODE, INTERNAL_SERVER_ERROR.getReasonPhrase(), "Custody signing failed"); } diff --git a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java index 058b3d29..9880c802 100644 --- a/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java +++ b/blockchain-custody/blockchain-custody/src/main/java/com/stablecoin/payments/custody/infrastructure/provider/dev/DevCustodyAdapter.java @@ -101,9 +101,13 @@ SignResult signAndSubmitEvm(SignRequest request) { var credentials = Credentials.create(properties.evmPrivateKey()); var usdcContract = chainConfig.usdcContract(); - var amountMinorUnits = request.amount() - .movePointRight(USDC_DECIMALS) - .toBigIntegerExact(); + var scaledAmount = request.amount().movePointRight(USDC_DECIMALS).stripTrailingZeros(); + if (scaledAmount.scale() > 0) { + throw new DevCustodyException( + "Amount has sub-minor-unit precision after scaling to %d decimals: %s" + .formatted(USDC_DECIMALS, request.amount())); + } + var amountMinorUnits = scaledAmount.toBigInteger(); var data = encodeErc20Transfer(request.toAddress(), amountMinorUnits); var nonce = request.nonce() != null ? BigInteger.valueOf(request.nonce()) : BigInteger.ZERO; diff --git a/blockchain-custody/blockchain-custody/src/main/resources/application-sandbox.yml b/blockchain-custody/blockchain-custody/src/main/resources/application-sandbox.yml index 5fc1354e..383b72cb 100644 --- a/blockchain-custody/blockchain-custody/src/main/resources/application-sandbox.yml +++ b/blockchain-custody/blockchain-custody/src/main/resources/application-sandbox.yml @@ -6,8 +6,8 @@ spring: datasource: url: jdbc:postgresql://localhost:5432/s4_blockchain_custody - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/compliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.yml b/compliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.yml index e84f1fc3..325feae8 100644 --- a/compliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.yml +++ b/compliance-travel-rule/compliance-travel-rule/src/main/resources/application-sandbox.yml @@ -35,8 +35,8 @@ app: spring: datasource: url: jdbc:postgresql://localhost:5432/s2_compliance - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4b45aeb0..4a231df6 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -148,12 +148,12 @@ services: ports: - "7233:7233" environment: - DB: postgresql + DB: postgres12 DB_PORT: 5432 POSTGRES_USER: dev POSTGRES_PWD: dev POSTGRES_SEEDS: postgres - DYNAMIC_CONFIG_FILE_PATH: /etc/temporal/config/dynamicconfig/development-sql.yaml + SKIP_DYNAMIC_CONFIG_SETUP: true depends_on: postgres: condition: service_healthy @@ -241,7 +241,7 @@ services: # Jaeger — distributed trace visualization (STA-221) # ───────────────────────────────────────────── jaeger: - image: jaegertracing/all-in-one:1.67 + image: jaegertracing/all-in-one:1.76.0 container_name: sp-jaeger ports: - "16686:16686" # Jaeger UI diff --git a/docker-compose.sandbox.yml b/docker-compose.sandbox.yml new file mode 100644 index 00000000..4ca9c366 --- /dev/null +++ b/docker-compose.sandbox.yml @@ -0,0 +1,398 @@ +# Sandbox E2E — Full Payment Sandwich with REAL external APIs +# +# Usage: +# make sandbox-build # Build Docker images +# make sandbox-up # Start infra + all 7 services +# make sandbox-status # Check health +# make sandbox-logs # Tail logs +# make sandbox-down # Stop everything +# +# Requires: .env.sandbox with real API keys (copy from .env.sandbox.template) + +services: + + # ── Infrastructure ───────────────────────────────────────────── + + postgres: + image: postgres:18-alpine + container_name: sb-postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: dev + POSTGRES_PASSWORD: dev + POSTGRES_DB: postgres + volumes: + - sb-pgdata:/var/lib/postgresql/data + - ./infra/local/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dev"] + interval: 5s + timeout: 5s + retries: 10 + + timescaledb: + image: timescale/timescaledb:latest-pg17 + container_name: sb-timescaledb + ports: + - "5433:5432" + environment: + POSTGRES_USER: dev + POSTGRES_PASSWORD: dev + POSTGRES_DB: fx_rates + deploy: + resources: + limits: + memory: 512M + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dev"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:8-alpine + container_name: sb-redis + ports: + - "6379:6379" + deploy: + resources: + limits: + memory: 256M + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + + redpanda: + image: docker.redpanda.com/redpandadata/redpanda:v24.3.18 + container_name: sb-redpanda + command: + - redpanda + - start + - --smp=1 + - --memory=512M + - --overprovisioned + - --kafka-addr=internal://0.0.0.0:29092,external://0.0.0.0:9092 + - --advertise-kafka-addr=internal://redpanda:29092,external://localhost:9092 + - --mode=dev-container + ports: + - "9092:9092" + deploy: + resources: + limits: + memory: 768M + healthcheck: + test: ["CMD-SHELL", "rpk cluster health --api-urls=localhost:9644 | grep -q 'Healthy.*true' || exit 1"] + interval: 10s + timeout: 10s + retries: 30 + + temporal: + image: temporalio/auto-setup:latest + container_name: sb-temporal + ports: + - "7233:7233" + environment: + DB: postgres12 + DB_PORT: 5432 + POSTGRES_USER: dev + POSTGRES_PWD: dev + POSTGRES_SEEDS: postgres + SKIP_DYNAMIC_CONFIG_SETUP: "true" + depends_on: + postgres: + condition: service_healthy + deploy: + resources: + limits: + memory: 512M + healthcheck: + test: ["CMD-SHELL", "temporal operator cluster health 2>/dev/null | grep -q 'OK' || exit 1"] + interval: 15s + timeout: 10s + retries: 15 + + # ── Application Services (Default Profile — env var overrides) ── + + compliance-travel-rule: + image: stablebridge/compliance-travel-rule:latest + container_name: sb-compliance + ports: + - "8083:8083" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + redpanda: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://postgres:5432/s2_compliance" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + REDIS_HOST: redis + REDIS_PORT: "6379" + APP_KYC_PROVIDER: persona + APP_KYC_PERSONA_API_KEY: ${PERSONA_SANDBOX_API_KEY} + APP_KYC_PERSONA_INQUIRY_TEMPLATE_ID: ${PERSONA_INQUIRY_TEMPLATE_ID} + APP_SANCTIONS_PROVIDER: ofac-sdn + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8083/compliance/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + + fx-liquidity-engine: + image: stablebridge/fx-liquidity-engine:latest + container_name: sb-fx-engine + ports: + - "8084:8084" + depends_on: + timescaledb: + condition: service_healthy + redis: + condition: service_healthy + redpanda: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://timescaledb:5432/fx_rates" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + REDIS_HOST: redis + REDIS_PORT: "6379" + APP_FX_RATE_PROVIDER: frankfurter + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8084/fx/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + + fiat-on-ramp: + image: stablebridge/fiat-on-ramp:latest + container_name: sb-onramp + ports: + - "8085:8085" + depends_on: + postgres: + condition: service_healthy + redpanda: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://postgres:5432/s3_fiat_on_ramp" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + APP_PSP_PROVIDER: stripe + APP_PSP_STRIPE_BASE_URL: "https://api.stripe.com" + APP_PSP_STRIPE_API_KEY: ${STRIPE_TEST_SECRET_KEY} + APP_PSP_STRIPE_TIMEOUT_SECONDS: "15" + APP_PSP_STRIPE_WEBHOOK_WEBHOOK_SECRET: ${STRIPE_SANDBOX_WEBHOOK_SECRET} + APP_PSP_STRIPE_WEBHOOK_TOLERANCE_SECONDS: "300" + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8085/on-ramp/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + + blockchain-custody: + image: stablebridge/blockchain-custody:latest + container_name: sb-custody + ports: + - "8086:8086" + depends_on: + postgres: + condition: service_healthy + redpanda: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://postgres:5432/s4_blockchain_custody" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + APP_CUSTODY_PROVIDER: dev + LOGGING_LEVEL_COM_STABLECOIN_PAYMENTS: DEBUG + APP_CUSTODY_DEV_EVM_PRIVATE_KEY: ${CUSTODY_DEV_EVM_PRIVATE_KEY} + APP_CUSTODY_DEV_BASE_RPC_URL: "https://base-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}" + APP_CUSTODY_DEV_ETHEREUM_RPC_URL: "https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}" + APP_CUSTODY_DEV_BASE_USDC_CONTRACT: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + APP_CUSTODY_DEV_ETHEREUM_USDC_CONTRACT: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" + ALCHEMY_API_KEY: ${ALCHEMY_API_KEY} + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8086/custody/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + + fiat-off-ramp: + image: stablebridge/fiat-off-ramp:latest + container_name: sb-offramp + ports: + - "8087:8087" + depends_on: + postgres: + condition: service_healthy + redpanda: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://postgres:5432/s5_fiat_off_ramp" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + APP_REDEMPTION_PROVIDER: circle + APP_REDEMPTION_CIRCLE_BASE_URL: "https://api-sandbox.circle.com" + APP_REDEMPTION_CIRCLE_API_KEY: ${CIRCLE_SANDBOX_API_KEY} + APP_REDEMPTION_CIRCLE_DESTINATION_ID: ${CIRCLE_SANDBOX_DESTINATION_ID} + APP_PAYOUT_PROVIDER: modulr + APP_PAYOUT_MODULR_BASE_URL: "https://api-sandbox.modulrfinance.com" + APP_PAYOUT_MODULR_API_KEY: ${MODULR_SANDBOX_API_KEY} + APP_PAYOUT_MODULR_SOURCE_ACCOUNT_ID: ${MODULR_SANDBOX_SOURCE_ACCOUNT_ID} + APP_PAYOUT_MODULR_WEBHOOK_WEBHOOK_SECRET: ${MODULR_SANDBOX_API_SECRET} + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8087/off-ramp/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + + ledger-accounting: + image: stablebridge/ledger-accounting:latest + container_name: sb-ledger + ports: + - "8088:8088" + depends_on: + postgres: + condition: service_healthy + redpanda: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://postgres:5432/s7_ledger_accounting" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8088/ledger/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + + payment-orchestrator: + image: stablebridge/payment-orchestrator:latest + container_name: sb-orchestrator + ports: + - "8082:8082" + depends_on: + postgres: + condition: service_healthy + redpanda: + condition: service_healthy + temporal: + condition: service_healthy + compliance-travel-rule: + condition: service_healthy + fx-liquidity-engine: + condition: service_healthy + environment: + JAVA_TOOL_OPTIONS: "-XX:+UseZGC -XX:MaxRAMPercentage=75.0 -Xss256k" + SPRING_CLOUD_VAULT_ENABLED: "false" + APP_SECURITY_ENABLED: "false" + APP_EXTERNAL_API_LOGGING_ENABLED: "${APP_EXTERNAL_API_LOGGING_ENABLED:-false}" + APP_FALLBACK_ADAPTERS_ENABLED: "true" + DB_URL: "jdbc:postgresql://postgres:5432/s1_payment_orchestrator" + DB_USER: dev + DB_PASS: dev + KAFKA_BROKERS: "redpanda:29092" + TEMPORAL_ADDRESS: "temporal:7233" + TEMPORAL_NAMESPACE: default + TEMPORAL_WORKER_ENABLED: "true" + COMPLIANCE_SERVICE_URL: "http://compliance-travel-rule:8083/compliance" + FX_SERVICE_URL: "http://fx-liquidity-engine:8084/fx" + ONRAMP_SERVICE_URL: "http://fiat-on-ramp:8085/on-ramp" + CUSTODY_SERVICE_URL: "http://blockchain-custody:8086/custody" + OFFRAMP_SERVICE_URL: "http://fiat-off-ramp:8087/off-ramp" + deploy: + resources: + limits: + memory: 512M + cpus: "0.5" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8082/orchestrator/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 120s + +volumes: + sb-pgdata: diff --git a/fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java b/fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java index 1461c162..398708cc 100644 --- a/fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java +++ b/fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapter.java @@ -18,6 +18,7 @@ import org.springframework.web.client.RestClient; import java.math.BigDecimal; +import java.math.RoundingMode; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.time.Duration; @@ -63,10 +64,16 @@ public RedemptionResult redeem(RedemptionRequest request) { log.info("[CIRCLE] Redeeming stablecoin payoutId={} stablecoin={} amount={}", request.payoutId(), request.stablecoin(), request.amount()); + var amount = request.amount(); + if (amount == null || amount.signum() <= 0) { + throw new IllegalArgumentException("Redemption amount must be greater than zero"); + } + var circleRequest = new CirclePayoutRequest( request.payoutId().toString(), new CirclePayoutRequest.CircleDestination("wire", properties.destinationId()), - new CirclePayoutRequest.CircleAmount(request.amount().toPlainString(), "USD") + new CirclePayoutRequest.CircleAmount( + amount.setScale(2, RoundingMode.DOWN).toPlainString(), "USD") ); var response = restClient.post() diff --git a/fiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.yml b/fiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.yml index 56fc7211..ac61b582 100644 --- a/fiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.yml +++ b/fiat-off-ramp/fiat-off-ramp/src/main/resources/application-sandbox.yml @@ -31,8 +31,8 @@ app: spring: datasource: url: jdbc:postgresql://localhost:5432/s5_fiat_off_ramp - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java b/fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java index 1fcd332c..24d96c79 100644 --- a/fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java +++ b/fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/infrastructure/provider/circle/CircleRedemptionAdapterTest.java @@ -51,7 +51,7 @@ void setUp() { } private RedemptionRequest aRedemptionRequest() { - return new RedemptionRequest(PAYOUT_ID, "USDC", new BigDecimal("10000.000000"), BigDecimal.ONE); + return new RedemptionRequest(PAYOUT_ID, "USDC", new BigDecimal("10000.00"), BigDecimal.ONE); } @Nested @@ -168,7 +168,7 @@ void redeem_verifiesRequestBodyAndAuth() { "data": { "id": "circle-payout-verify", "amount": { - "amount": "10000.000000", + "amount": "10000.00", "currency": "USD" }, "status": "pending", @@ -190,7 +190,7 @@ void redeem_verifiesRequestBodyAndAuth() { "id": "%s" }, "amount": { - "amount": "10000.000000", + "amount": "10000.00", "currency": "USD" } } @@ -215,6 +215,40 @@ void redeem_serverError() { .isInstanceOf(Exception.class); } + @Test + @DisplayName("should truncate outbound amount to 2 decimals for Circle request") + void redeem_truncatesAmountToTwoDecimals() { + wireMock.stubFor(post(urlEqualTo("/v1/businessAccount/payouts")) + .willReturn(aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(""" + { + "data": { + "id": "circle-payout-truncate", + "amount": { + "amount": "10000.00", + "currency": "USD" + }, + "status": "pending", + "createDate": "2026-03-10T12:00:00.000Z" + } + } + """))); + + var request = new RedemptionRequest(PAYOUT_ID, "USDC", new BigDecimal("10000.009"), BigDecimal.ONE); + adapter.redeem(request); + + wireMock.verify(postRequestedFor(urlEqualTo("/v1/businessAccount/payouts")) + .withRequestBody(equalToJson(""" + { + "idempotencyKey": "%s", + "destination": { "type": "wire", "id": "%s" }, + "amount": { "amount": "10000.00", "currency": "USD" } + } + """.formatted(PAYOUT_ID, DESTINATION_ID)))); + } + @Test @DisplayName("should handle EUR currency in Circle response") void redeem_eurCurrency() { diff --git a/fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java b/fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java index 60fa31a7..05345bcc 100644 --- a/fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java +++ b/fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java @@ -64,7 +64,6 @@ public PspPaymentResult initiatePayment(PspPaymentRequest request) { formData.add("amount", toMinorUnits(request)); formData.add("currency", request.amount().currency().toLowerCase()); formData.add("payment_method_types[]", "us_bank_account"); - formData.add("confirm", "true"); formData.add("metadata[collection_id]", request.collectionId().toString()); var requestSpec = restClient.post() diff --git a/fiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.yml b/fiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.yml index 4440883c..3a82d078 100644 --- a/fiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.yml +++ b/fiat-on-ramp/fiat-on-ramp/src/main/resources/application-sandbox.yml @@ -29,8 +29,8 @@ app: spring: datasource: url: jdbc:postgresql://localhost:5432/s3_fiat_on_ramp - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java b/fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java index 89ae2219..4c7320ce 100644 --- a/fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java +++ b/fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java @@ -186,7 +186,6 @@ void initiatePayment_verifiesRequestBody() { .withRequestBody(containing("amount=25000")) .withRequestBody(containing("currency=usd")) .withRequestBody(containing("payment_method_types%5B%5D=us_bank_account")) - .withRequestBody(containing("confirm=true")) .withRequestBody(containing("metadata%5Bcollection_id%5D=" + COLLECTION_ID))); } } diff --git a/fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java b/fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java index f979b731..5b00210a 100644 --- a/fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java +++ b/fx-liquidity-engine/fx-liquidity-engine/src/main/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJob.java @@ -3,6 +3,7 @@ import com.stablecoin.payments.fx.domain.model.Corridor; import com.stablecoin.payments.fx.domain.model.RateSnapshot; import com.stablecoin.payments.fx.domain.model.RateSourceType; +import com.stablecoin.payments.fx.domain.port.LiquidityPoolRepository; import com.stablecoin.payments.fx.domain.port.RateCache; import com.stablecoin.payments.fx.domain.port.RateHistoryRepository; import com.stablecoin.payments.fx.domain.port.RateProvider; @@ -12,29 +13,27 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.util.List; - @Slf4j @Component @RequiredArgsConstructor @ConditionalOnProperty(name = "app.fx.rate-refresh.enabled", havingValue = "true", matchIfMissing = true) public class RateRefreshJob { - static final List SUPPORTED_CORRIDORS = List.of( - new Corridor("USD", "EUR"), - new Corridor("EUR", "USD") - ); - private final RateProvider rateProvider; private final RateCache rateCache; private final RateHistoryRepository rateHistoryRepository; + private final LiquidityPoolRepository liquidityPoolRepository; @Scheduled(fixedDelayString = "${app.fx.rate-refresh.interval-ms:5000}") public void refreshRates() { - log.debug("Starting rate refresh for {} corridors", SUPPORTED_CORRIDORS.size()); + var corridors = liquidityPoolRepository.findAll().stream() + .map(pool -> new Corridor(pool.fromCurrency(), pool.toCurrency())) + .toList(); + + log.debug("Starting rate refresh for {} corridors", corridors.size()); int refreshed = 0; - for (var corridor : SUPPORTED_CORRIDORS) { + for (var corridor : corridors) { try { var rateOpt = rateProvider.getRate(corridor.fromCurrency(), corridor.toCurrency()); if (rateOpt.isPresent()) { @@ -52,6 +51,6 @@ public void refreshRates() { } } - log.debug("Rate refresh complete: {}/{} corridors updated", refreshed, SUPPORTED_CORRIDORS.size()); + log.debug("Rate refresh complete: {}/{} corridors updated", refreshed, corridors.size()); } } diff --git a/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.yml b/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.yml index 222af55e..c7d541d4 100644 --- a/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.yml +++ b/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/application-sandbox.yml @@ -18,8 +18,8 @@ app: spring: datasource: url: jdbc:postgresql://localhost:5433/fx_rates - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql b/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql new file mode 100644 index 00000000..df443a47 --- /dev/null +++ b/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V6__seed_usd_gbp_corridor.sql @@ -0,0 +1,13 @@ +-- ============================================================ +-- V6: Seed USD-GBP liquidity pool for US->GB corridor +-- ============================================================ + +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) +ON CONFLICT (from_currency, to_currency) DO NOTHING; diff --git a/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V7__seed_gbp_usd_corridor.sql b/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V7__seed_gbp_usd_corridor.sql new file mode 100644 index 00000000..31fef2c8 --- /dev/null +++ b/fx-liquidity-engine/fx-liquidity-engine/src/main/resources/db/migration/V7__seed_gbp_usd_corridor.sql @@ -0,0 +1,13 @@ +-- ============================================================ +-- V7: Seed reverse GBP-USD liquidity pool for GB->US corridor +-- ============================================================ + +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(), 'GBP', 'USD', + 1000000.00000000, 0.00000000, + 100000.00000000, 5000000.00000000, + now(), 0) +ON CONFLICT (from_currency, to_currency) DO NOTHING; diff --git a/fx-liquidity-engine/fx-liquidity-engine/src/test/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJobTest.java b/fx-liquidity-engine/fx-liquidity-engine/src/test/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJobTest.java index febb9b96..767c53c6 100644 --- a/fx-liquidity-engine/fx-liquidity-engine/src/test/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJobTest.java +++ b/fx-liquidity-engine/fx-liquidity-engine/src/test/java/com/stablecoin/payments/fx/infrastructure/scheduling/RateRefreshJobTest.java @@ -1,11 +1,14 @@ package com.stablecoin.payments.fx.infrastructure.scheduling; import com.stablecoin.payments.fx.domain.model.CorridorRate; +import com.stablecoin.payments.fx.domain.model.LiquidityPool; import com.stablecoin.payments.fx.domain.model.RateSnapshot; import com.stablecoin.payments.fx.domain.model.RateSourceType; +import com.stablecoin.payments.fx.domain.port.LiquidityPoolRepository; import com.stablecoin.payments.fx.domain.port.RateCache; import com.stablecoin.payments.fx.domain.port.RateHistoryRepository; import com.stablecoin.payments.fx.domain.port.RateProvider; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -13,6 +16,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.math.BigDecimal; +import java.util.List; import java.util.Optional; import static com.stablecoin.payments.fx.fixtures.CorridorRateFixtures.aUsdEurRate; @@ -33,9 +38,24 @@ class RateRefreshJobTest { @Mock private RateHistoryRepository rateHistoryRepository; + @Mock + private LiquidityPoolRepository liquidityPoolRepository; + @InjectMocks private RateRefreshJob rateRefreshJob; + @BeforeEach + void setUp() { + given(liquidityPoolRepository.findAll()).willReturn(List.of( + aPool("USD", "EUR"), + aPool("EUR", "USD") + )); + } + + private static LiquidityPool aPool(String from, String to) { + return LiquidityPool.create(from, to, new BigDecimal("1000000"), new BigDecimal("100000"), new BigDecimal("5000000")); + } + @Test @DisplayName("should refresh rates and record to cache and history for available corridors") void refreshesRatesForAvailableCorridors() { diff --git a/ledger-accounting/ledger-accounting/src/main/resources/application-sandbox.yml b/ledger-accounting/ledger-accounting/src/main/resources/application-sandbox.yml index 7f865ed8..b99b5e6b 100644 --- a/ledger-accounting/ledger-accounting/src/main/resources/application-sandbox.yml +++ b/ledger-accounting/ledger-accounting/src/main/resources/application-sandbox.yml @@ -19,8 +19,8 @@ app: spring: datasource: url: jdbc:postgresql://localhost:5432/s7_ledger_accounting - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/merchant-iam/merchant-iam/src/main/resources/application-sandbox.yml b/merchant-iam/merchant-iam/src/main/resources/application-sandbox.yml index 08d28ae0..82602568 100644 --- a/merchant-iam/merchant-iam/src/main/resources/application-sandbox.yml +++ b/merchant-iam/merchant-iam/src/main/resources/application-sandbox.yml @@ -24,8 +24,8 @@ merchant-iam: spring: datasource: url: jdbc:postgresql://localhost:5432/s13_merchant_iam - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: diff --git a/merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java b/merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java index 9204f3d2..1293c3b6 100644 --- a/merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java +++ b/merchant-onboarding/merchant-onboarding/src/main/java/com/stablecoin/payments/merchant/onboarding/application/config/FallbackAdaptersConfig.java @@ -8,6 +8,7 @@ import com.stablecoin.payments.merchant.onboarding.infrastructure.kyb.MockCompanyRegistryAdapter; import com.stablecoin.payments.merchant.onboarding.infrastructure.kyb.MockKybAdapter; import com.stablecoin.payments.merchant.onboarding.infrastructure.temporal.adapter.MockOnboardingWorkflowAdapter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,21 +30,25 @@ public class FallbackAdaptersConfig { @Bean + @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(); } diff --git a/merchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.yml b/merchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.yml index 0e6f7742..d8bacc9c 100644 --- a/merchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.yml +++ b/merchant-onboarding/merchant-onboarding/src/main/resources/application-sandbox.yml @@ -19,8 +19,8 @@ app: spring: datasource: url: jdbc:postgresql://localhost:5432/s11_merchant_onboarding - username: sp_user - password: sp_pass + username: dev + password: dev cloud: stream: diff --git a/payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java b/payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java index d21d7ba0..1b89e764 100644 --- a/payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java +++ b/payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/activity/OffRampActivityImpl.java @@ -10,6 +10,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + import static com.stablecoin.payments.orchestrator.domain.workflow.activity.OffRampResult.OffRampStatus.FAILED; import static com.stablecoin.payments.orchestrator.domain.workflow.activity.OffRampResult.OffRampStatus.INITIATED; @@ -44,14 +49,14 @@ public OffRampResult initiatePayout(OffRampRequest request) { request.targetCurrency(), request.appliedFxRate(), request.recipientId(), - "sha256:" + request.recipientId(), - "SEPA", + sha256(request.recipientId().toString()), + resolvePaymentRail(request.targetCurrency()), "modulr-default", "Modulr", - "DE89370400440532013000", - "COBADEFFXXX", - "IBAN", - "DE", + resolveBankAccount(request.targetCurrency()), + resolveBankCode(request.targetCurrency()), + resolveAccountType(request.targetCurrency()), + resolveCountry(request.targetCurrency()), null, null, null ); @@ -76,4 +81,54 @@ public OffRampResult initiatePayout(OffRampRequest request) { return new OffRampResult(null, FAILED, e.getMessage()); } } + + 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); + }; + } + + static String sha256(String input) { + try { + var digest = MessageDigest.getInstance("SHA-256"); + var hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return "sha256:" + HexFormat.of().formatHex(hash); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } } diff --git a/payment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml b/payment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml index 7a59ac7a..e5c9a17e 100644 --- a/payment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml +++ b/payment-orchestrator/payment-orchestrator/src/main/resources/application-sandbox.yml @@ -33,8 +33,8 @@ temporal: spring: datasource: url: jdbc:postgresql://localhost:5432/s1_payment_orchestrator - username: sp_user - password: sp_pass + username: dev + password: dev cloud: vault: