Skip to content

feat(s5): project scaffold — fiat-off-ramp (STA-146)#150

Merged
Puneethkumarck merged 3 commits into
mainfrom
feature/STA-146-s5-fiat-off-ramp-scaffold
Mar 9, 2026
Merged

feat(s5): project scaffold — fiat-off-ramp (STA-146)#150
Puneethkumarck merged 3 commits into
mainfrom
feature/STA-146-s5-fiat-off-ramp-scaffold

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Bootstrap S5 Fiat Off-Ramp service with 3 Gradle modules (fiat-off-ramp-api, fiat-off-ramp-client, fiat-off-ramp)
  • Flyway V1 migration: 6 tables (payout_orders, stablecoin_redemptions, off_ramp_transactions, 3 Namastack outbox)
  • SecurityConfig, IdempotencyKeyFilter, CorrelationIdFilter, GlobalExceptionHandler (OF-XXXX error codes)
  • 8 ArchUnit tests green, AbstractIntegrationTest + TestUtils in testFixtures
  • Base package: com.stablecoin.payments.offramp, port 8087, context path /off-ramp

Closes STA-146

Test plan

  • 8 ArchUnit tests pass
  • spotlessCheck passes (all 3 modules)
  • Full check task passes (including JaCoCo)
  • CI pipeline green

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Fiat off-ramp API service enabling payout initiation and status retrieval
    • Structured error responses with detailed validation feedback
    • Event-driven payout lifecycle tracking for initiation, completion, failure, and stablecoin redemption
    • Request correlation tracking across service calls via headers
    • Idempotency support for API operations to ensure safe retries

@Puneethkumarck Puneethkumarck added phase-3 Value Movement MVP service-s5 Off-Ramp feature New feature labels Mar 9, 2026
@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Puneethkumarck has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2b9fab71-ceeb-4c15-acab-ba1b7b503b44

📥 Commits

Reviewing files that changed from the base of the PR and between 001db98 and 0e77ee7.

📒 Files selected for processing (26)
  • .github/workflows/claude-pr-review.yml
  • fiat-off-ramp/fiat-off-ramp-api/build.gradle.kts
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/ApiError.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/PayoutRequest.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/PayoutResponse.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutCompleted.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutFailed.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutInitiated.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/StablecoinRedeemed.java
  • fiat-off-ramp/fiat-off-ramp-client/build.gradle.kts
  • fiat-off-ramp/fiat-off-ramp-client/src/main/java/com/stablecoin/payments/offramp/client/FiatOffRampClient.java
  • fiat-off-ramp/fiat-off-ramp/build.gradle.kts
  • fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java
  • fiat-off-ramp/fiat-off-ramp/src/integration-test/resources/application-integration-test.yml
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/FiatOffRampApplication.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/controller/GlobalExceptionHandler.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/filter/CorrelationIdFilter.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/filter/IdempotencyKeyFilter.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/config/FallbackAdaptersConfig.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/config/SecurityConfig.java
  • fiat-off-ramp/fiat-off-ramp/src/main/resources/application.yml
  • fiat-off-ramp/fiat-off-ramp/src/main/resources/db/migration/V1__initial_schema.sql
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/config/TestKafkaConfig.java
  • fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/TestUtils.java
  • settings.gradle.kts

Walkthrough

This PR introduces a new fiat off-ramp payment module with three sub-modules: API contracts defining payouts and events, a Feign client for service-to-service communication, and the main Spring Boot service with database schema, integration infrastructure, security filters, and comprehensive test scaffolding.

Changes

Cohort / File(s) Summary
API Module Setup
fiat-off-ramp/fiat-off-ramp-api/build.gradle.kts, fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/...
Gradle configuration and public API contracts: ApiError with factory methods, PayoutRequest with Jakarta validation annotations, PayoutResponse response DTO, and domain events (FiatPayoutInitiated, FiatPayoutCompleted, FiatPayoutFailed, StablecoinRedeemed) as records with schema versioning constants.
Client Module
fiat-off-ramp/fiat-off-ramp-client/build.gradle.kts, fiat-off-ramp/fiat-off-ramp-client/src/main/java/.../FiatOffRampClient.java
Gradle configuration with Spring Cloud OpenFeign dependency and FiatOffRampClient Feign interface exposing POST /v1/payouts and GET /v1/payouts/{payoutId} endpoints.
Service Build & Project Setup
settings.gradle.kts, fiat-off-ramp/fiat-off-ramp/build.gradle.kts
Multi-module Gradle configuration including test source sets (integrationTest, businessTest) with separate classpaths, Jacoco coverage verification (0.50 threshold), Jib container build, and extensive dependencies (Spring Boot, Kafka, Resilience4j, MapStruct, Testcontainers, WireMock).
Spring Configuration & Security
src/main/java/.../FiatOffRampApplication.java, src/main/java/.../config/SecurityConfig.java, src/main/java/.../config/FallbackAdaptersConfig.java
Spring Boot entry point with @EnableFeignClients and @EnableScheduling, conditional SecurityFilterChain (enabled/disabled via property), and fallback Clock bean. Security note: app.security.enabled defaults to disabled—verify intended behavior.
Request Handling & Filters
src/main/java/.../controller/GlobalExceptionHandler.java, src/main/java/.../filter/CorrelationIdFilter.java, src/main/java/.../filter/IdempotencyKeyFilter.java
Global exception handler mapping validation/type errors to OF-0001 (BAD_REQUEST), domain errors to OF-0002 (CONFLICT), and unexpected exceptions to OF-9999 (INTERNAL_SERVER_ERROR). CorrelationIdFilter propagates/generates X-Correlation-Id. IdempotencyKeyFilter enforces Idempotency-Key for POST/PATCH/DELETE except /actuator/ and /internal/webhooks/.
Application & Integration Test Configuration
src/main/resources/application.yml, src/integration-test/resources/application-integration-test.yml, src/integration-test/java/.../AbstractIntegrationTest.java
Application configuration with PostgreSQL, Kafka, Namastack Outbox (50ms poll), and Swagger/SpringDoc setup. Integration test profile overrides JPA ddl-auto to validate, configures Testcontainers with PostgreSQL and Kafka, and provides table truncation fixture in AbstractIntegrationTest.
Database Schema & Migration
src/main/resources/db/migration/V1__initial_schema.sql
Initial Flyway migration creating payout_orders (with partner-reference conditional index), stablecoin_redemptions, off_ramp_transactions, and Namastack outbox tables with standard timestamps, foreign keys, and indexes. Includes runtime privilege guard for sp_readonly role.
Test Infrastructure & Architecture
src/test/java/.../arch/ArchitectureTest.java, src/test/java/.../config/TestKafkaConfig.java, src/testFixtures/java/.../TestUtils.java
ArchUnit tests enforcing layering (domain ⊥ Spring, domain ⊥ JPA/infra/application, controllers ⊂ application.controller, ports are interfaces, events are records). TestKafkaConfig stub for unit tests. TestUtils provides eqIgnoringTimestamps() and eqIgnoring() matchers using AssertJ recursive comparison for test assertions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

test, infrastructure

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: bootstrapping the fiat-off-ramp service project with 3 Gradle modules, which is the core objective.
Description check ✅ Passed The PR description comprehensively covers the summary, related issue, type of change (new feature), what changed, test results, and includes self-verification checklist items. All required template sections are populated with relevant details.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/STA-146-s5-fiat-off-ramp-scaffold

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

@Puneethkumarck Puneethkumarck force-pushed the feature/STA-146-s5-fiat-off-ramp-scaffold branch from 001db98 to 2a5738b Compare March 9, 2026 22:02

@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: 19

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

Inline comments:
In
`@fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/ApiError.java`:
- Around line 16-18: Normalize and defensively copy the errors map in
ApiError.withErrors: if the provided errors is null, replace it with an empty
map; otherwise create a new map copying each key and making an unmodifiable copy
of each List<String> value (so callers can't mutate lists after construction),
then pass that immutable/deep-copied map into the ApiError constructor (i.e.,
modify ApiError.withErrors to return new ApiError(code, status, message,
<immutable deep copy of errors>)).

In
`@fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutInitiated.java`:
- Around line 7-22: The FiatPayoutInitiated record exposes schemaVersion and
eventType as constructor parameters despite having fixed constants; add a
compact constructor (or static factory) in FiatPayoutInitiated that ignores
caller-supplied schemaVersion/eventType and enforces schemaVersion =
SCHEMA_VERSION and eventType = EVENT_TYPE (validate or overwrite incoming
values), and apply the same pattern to the sibling event records so nobody can
instantiate these records with mismatched metadata.

In
`@fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/PayoutRequest.java`:
- Around line 14-23: PayoutRequest currently only uses `@NotBlank` for several
tightly constrained fields so invalid lengths or unsupported enum values slip
through to persistence; update the PayoutRequest DTO (fields payoutType,
stablecoin, targetCurrency, recipientAccountHash, paymentRail, offRampPartnerId,
offRampPartnerName) to enforce the same constraints as the DB schema by adding
`@Size`(max=...) with the DB column lengths and use either `@Pattern` or a custom
`@Enum` validator (or javax.validation.Constraint backed by an Enum) for fields
with limited allowed values (e.g., payoutType, stablecoin, paymentRail) so
invalid inputs fail Bean Validation and return the OF-0001 response rather than
failing at persistence.

In
`@fiat-off-ramp/fiat-off-ramp-client/src/main/java/com/stablecoin/payments/offramp/client/FiatOffRampClient.java`:
- Around line 13-20: The Feign client FiatOffRampClient is missing the service
context path so requests to initiatePayout and getPayout will target /v1/...
instead of /off-ramp/v1/.... Update the `@FeignClient` annotation (on interface
FiatOffRampClient) to include the path attribute for the service context (e.g.,
path="/off-ramp") so the client-level base path is applied and both
initiatePayout and getPayout resolve to /off-ramp/v1/payouts.

In `@fiat-off-ramp/fiat-off-ramp/build.gradle.kts`:
- Around line 25-26: The build file is incorrectly adding
sourceSets.test.get().output to higher-level classpaths (compileClasspath and
runtimeClasspath), which exposes unit-test-only helpers to integration/business
suites; remove the "+ sourceSets.test.get().output" from the compileClasspath
and runtimeClasspath assignments in the blocks that modify those classpaths (and
the similar occurrences around the 46-47 region), leaving only
sourceSets.main.get().output, and instead add explicit testFixtures dependencies
where shared test helpers are needed (use testFixtures(project) or
implementation(testFixtures(project)) on the appropriate source sets) so shared
fixtures come from the testFixtures API rather than src/test output.
- Around line 111-112: Remove the non-existent Spring Boot test starters and
correct the test dependency coordinates: delete any usages of
"spring-boot-starter-webmvc-test" and "spring-boot-starter-security-test" and
instead rely on the existing "spring-boot-starter-test" for web testing and add
the explicit security test artifact
"org.springframework.security:spring-security-test" to the testImplementation
dependencies; also confirm whether
"implementation(\"org.springframework.boot:spring-boot-starter-flyway\")" in the
build script is intentional—if you only need Flyway itself, replace it with the
canonical "org.flywaydb:flyway-core" (or keep the starter only if you
intentionally want the Spring Boot starter integration).

In
`@fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java`:
- Around line 38-48: In AbstractIntegrationTest, the cleanDatabase() method
currently truncates off_ramp_transactions, stablecoin_redemptions,
payout_orders, and offramp_outbox_record but omits Namastack outbox tables;
update the SQL passed to jdbcTemplate.execute in cleanDatabase() to also include
offramp_outbox_instance and offramp_outbox_partition in the TRUNCATE TABLE list
(with CASCADE) so instance heartbeats and partition assignments are cleared
between tests.

In
`@fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/controller/GlobalExceptionHandler.java`:
- Around line 47-58: Replace the generic JDK exception handlers in
GlobalExceptionHandler: remove or stop mapping IllegalStateException and
IllegalArgumentException in handleIllegalState and handleIllegalArgument and
instead create and handle domain-specific exceptions (e.g.,
InvalidTransitionException, InvalidParameterException) that encapsulate safe,
fixed client-facing messages; update the controller/service code to throw those
domain exceptions where appropriate and have the GlobalExceptionHandler return
ApiError.of with those fixed messages and codes (e.g., "OF-0002"/"OF-0001")
while allowing unmapped runtime exceptions to propagate to the 500 handler.

In
`@fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/filter/CorrelationIdFilter.java`:
- Around line 28-33: The CorrelationIdFilter currently accepts any
CORRELATION_ID_HEADER value and puts it into MDC (CORRELATION_ID_MDC_KEY) and
the response; update the filter so it attempts to parse the incoming header as a
java.util.UUID (e.g., UUID.fromString) and if parsing fails (or header is
null/blank) generate a new UUID; always store and return the normalized
UUID.toString() in MDC and response header so downstream code and logs only ever
see valid UUIDs.

In
`@fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/filter/IdempotencyKeyFilter.java`:
- Around line 38-48: The filter currently accepts the Idempotency-Key header
as-is in requiresIdempotencyKey(...) flow using IDEMPOTENCY_KEY_HEADER; add
format validation (e.g., call a helper isValidUuid(String) or a
validateIdempotencyKey method) after retrieving key and before proceeding: if
the key is null/blank or fails isValidUuid(key) (or exceeds acceptable
length/contains illegal chars) then log a clear info/warn including request
method/URI, set response status to HttpStatus.BAD_REQUEST, set
MediaType.APPLICATION_JSON_VALUE and write the existing error JSON (adjust
message to indicate invalid Idempotency-Key format), then return; ensure the
helper is referenced from the same class or a util and used in the same branch
that currently handles missing keys.
- Around line 44-46: The filter currently writes hard-coded JSON in
IdempotencyKeyFilter via response.getWriter().write(...), which is fragile and
inconsistent; instead inject an ObjectMapper into IdempotencyKeyFilter,
construct an ApiError instance (with code "OF-0001", status "Bad Request", the
same message and empty errors), set the response status and content-type, and
serialize the ApiError to the response using
objectMapper.writeValue(response.getWriter(), apiError) so all error responses
remain consistent with GlobalExceptionHandler.

In
`@fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/config/SecurityConfig.java`:
- Around line 18-28: The SecurityFilterChain built in
securityFilterChain(HttpSecurity) currently stops at
.anyRequest().authenticated() without wiring any authentication mechanism;
update securityFilterChain to register an authentication scheme or filter (e.g.,
call http.oauth2ResourceServer(...) or http.httpBasic(), or add a custom filter
with http.addFilterBefore(...)) and also set a custom AuthenticationEntryPoint
and AccessDeniedHandler so 401/403 responses follow your API error contract;
target the securityFilterChain method and HttpSecurity configuration where
.anyRequest().authenticated() is set and wire the chosen auth provider/filter
plus the custom entry point and access denied handler.
- Line 23: The current SecurityConfig allows unauthenticated access to all
actuator endpoints via .requestMatchers("/actuator/**").permitAll(), which
exposes sensitive metrics/prometheus data; update the security configuration in
SecurityConfig (the HttpSecurity/SecurityFilterChain setup where
.requestMatchers is called) to only permit the specific safe endpoints (e.g.,
"/actuator/health" and "/actuator/info") instead of the wildcard, or
alternatively move management endpoints to a separate management port and secure
the main application endpoints accordingly; adjust the .requestMatchers(...)
call in SecurityConfig to list only the allowed paths or implement a separate
management server security configuration.
- Around line 31-38: The application.yml currently sets app.security.enabled:
false which causes the insecure bean localSecurityFilterChain (in
SecurityConfig) to load by default; change the default in application.yml to
true so the secure securityFilterChain (with matchIfMissing = true) is used
unless overridden, and ensure only test/local environments set
APP_SECURITY_ENABLED=false via docker-compose or integration-test configs that
already do so.

In `@fiat-off-ramp/fiat-off-ramp/src/main/resources/application.yml`:
- Around line 117-119: Replace the hardcoded property app.security.enabled:
false with an environment-backed property so the default is secure and can be
overridden; change the value to reference ${APP_SECURITY_ENABLED:true} (property
name APP_SECURITY_ENABLED) in the main application.yml and add an
application-prod.yml profile that explicitly sets app.security.enabled: true to
enforce security in production. Ensure the property key app.security.enabled is
used consistently across config and any code that reads it.

In
`@fiat-off-ramp/fiat-off-ramp/src/main/resources/db/migration/V1__initial_schema.sql`:
- Around line 107-108: Rename the index idx_offramp_outbox_record_status to
follow the <table>_<columns>_idx convention by creating it as
offramp_outbox_record_status_next_retry_at_idx on table offramp_outbox_record
for columns (status, next_retry_at); update the CREATE INDEX statement
accordingly and ensure any references (drops or constraints) use the new name so
V1 uses the consistent naming convention.
- Around line 33-42: Remove the redundant index creation for payment_id: the
UNIQUE constraint payout_orders_payment_id_unique on table payout_orders already
creates an index, so delete the CREATE INDEX payout_orders_payment_id_idx ON
payout_orders (payment_id) statement to avoid duplicate index/write overhead;
keep the UNIQUE constraint as-is.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java`:
- Around line 28-37: Update the test display name in ArchitectureTest for the
domainShouldNotDependOnSpring rule to list all three Spring exemptions; change
the `@DisplayName` on the domainShouldNotDependOnSpring test to explicitly mention
"stereotype, transaction, and beans.factory.annotation" so it matches the
resideOutsideOfPackage exemptions (org.springframework.stereotype..,
org.springframework.transaction..,
org.springframework.beans.factory.annotation..).

In
`@fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/TestUtils.java`:
- Around line 24-33: The helper in TestUtils currently catches Throwable and
hides serious errors; change the catch to only catch AssertionError (the
AssertJ/JUnit comparison failure) and return false there, and let all other
Throwables/Errors propagate (i.e., rethrow or don't catch them). Locate the
matcher helper in TestUtils.java that compares original and expected (it
references original, expected, fieldsToIgnore, and uses
usingRecursiveComparison()) and replace the catch(Throwable t) block with a
catch(AssertionError ae) { return false; } so only assertion failures become
false and other errors are not swallowed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4774566f-6379-4a54-ae9b-93769721be16

📥 Commits

Reviewing files that changed from the base of the PR and between b0d1047 and 001db98.

📒 Files selected for processing (25)
  • fiat-off-ramp/fiat-off-ramp-api/build.gradle.kts
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/ApiError.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/PayoutRequest.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/PayoutResponse.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutCompleted.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutFailed.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutInitiated.java
  • fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/StablecoinRedeemed.java
  • fiat-off-ramp/fiat-off-ramp-client/build.gradle.kts
  • fiat-off-ramp/fiat-off-ramp-client/src/main/java/com/stablecoin/payments/offramp/client/FiatOffRampClient.java
  • fiat-off-ramp/fiat-off-ramp/build.gradle.kts
  • fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java
  • fiat-off-ramp/fiat-off-ramp/src/integration-test/resources/application-integration-test.yml
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/FiatOffRampApplication.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/controller/GlobalExceptionHandler.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/filter/CorrelationIdFilter.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/filter/IdempotencyKeyFilter.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/config/FallbackAdaptersConfig.java
  • fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/config/SecurityConfig.java
  • fiat-off-ramp/fiat-off-ramp/src/main/resources/application.yml
  • fiat-off-ramp/fiat-off-ramp/src/main/resources/db/migration/V1__initial_schema.sql
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/config/TestKafkaConfig.java
  • fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/TestUtils.java
  • settings.gradle.kts

Comment on lines +16 to +18
public static ApiError withErrors(String code, String status, String message,
Map<String, List<String>> errors) {
return new ApiError(code, status, message, errors);

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

Normalize and defensively copy errors.

withErrors(...) currently preserves null and the caller's mutable collections. That can produce errors: null even though GlobalExceptionHandler and IdempotencyKeyFilter both shape empty errors as {}, and later mutations can change the serialized payload after construction.

Suggested hardening
 public static ApiError withErrors(String code, String status, String message,
                                   Map<String, List<String>> errors) {
-    return new ApiError(code, status, message, errors);
+    var safeErrors = errors == null
+            ? Map.<String, List<String>>of()
+            : errors.entrySet().stream()
+                    .collect(java.util.stream.Collectors.toUnmodifiableMap(
+                            Map.Entry::getKey,
+                            entry -> List.copyOf(entry.getValue())
+                    ));
+    return new ApiError(code, status, message, safeErrors);
 }
📝 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
public static ApiError withErrors(String code, String status, String message,
Map<String, List<String>> errors) {
return new ApiError(code, status, message, errors);
public static ApiError withErrors(String code, String status, String message,
Map<String, List<String>> errors) {
var safeErrors = errors == null
? Map.<String, List<String>>of()
: errors.entrySet().stream()
.collect(java.util.stream.Collectors.toUnmodifiableMap(
Map.Entry::getKey,
entry -> List.copyOf(entry.getValue())
));
return new ApiError(code, status, message, safeErrors);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/ApiError.java`
around lines 16 - 18, Normalize and defensively copy the errors map in
ApiError.withErrors: if the provided errors is null, replace it with an empty
map; otherwise create a new map copying each key and making an unmodifiable copy
of each List<String> value (so callers can't mutate lists after construction),
then pass that immutable/deep-copied map into the ApiError constructor (i.e.,
modify ApiError.withErrors to return new ApiError(code, status, message,
<immutable deep copy of errors>)).

Comment on lines +7 to +22
public record FiatPayoutInitiated(
int schemaVersion,
UUID eventId,
String eventType,
UUID payoutId,
UUID paymentId,
UUID correlationId,
BigDecimal fiatAmount,
String targetCurrency,
String paymentRail,
String partner,
Instant initiatedAt
) {
public static final String EVENT_TYPE = "fiat.payout.initiated";
public static final int SCHEMA_VERSION = 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Enforce the fixed event metadata at construction time.

This type already defines EVENT_TYPE and SCHEMA_VERSION, but callers can still instantiate it with different values. That makes it easy to publish a FiatPayoutInitiated payload with mismatched contract metadata. Add a compact constructor or a factory that pins those values, and apply the same pattern to the sibling event records.

Proposed fix
 public record FiatPayoutInitiated(
         int schemaVersion,
         UUID eventId,
         String eventType,
         UUID payoutId,
         UUID paymentId,
         UUID correlationId,
         BigDecimal fiatAmount,
         String targetCurrency,
         String paymentRail,
         String partner,
         Instant initiatedAt
 ) {
     public static final String EVENT_TYPE = "fiat.payout.initiated";
     public static final int SCHEMA_VERSION = 1;
+
+    public FiatPayoutInitiated {
+        if (schemaVersion != SCHEMA_VERSION) {
+            throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
+        }
+        if (!EVENT_TYPE.equals(eventType)) {
+            throw new IllegalArgumentException("Unexpected event type: " + eventType);
+        }
+    }
 }
📝 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
public record FiatPayoutInitiated(
int schemaVersion,
UUID eventId,
String eventType,
UUID payoutId,
UUID paymentId,
UUID correlationId,
BigDecimal fiatAmount,
String targetCurrency,
String paymentRail,
String partner,
Instant initiatedAt
) {
public static final String EVENT_TYPE = "fiat.payout.initiated";
public static final int SCHEMA_VERSION = 1;
}
public record FiatPayoutInitiated(
int schemaVersion,
UUID eventId,
String eventType,
UUID payoutId,
UUID paymentId,
UUID correlationId,
BigDecimal fiatAmount,
String targetCurrency,
String paymentRail,
String partner,
Instant initiatedAt
) {
public static final String EVENT_TYPE = "fiat.payout.initiated";
public static final int SCHEMA_VERSION = 1;
public FiatPayoutInitiated {
if (schemaVersion != SCHEMA_VERSION) {
throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
}
if (!EVENT_TYPE.equals(eventType)) {
throw new IllegalArgumentException("Unexpected event type: " + eventType);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/events/FiatPayoutInitiated.java`
around lines 7 - 22, The FiatPayoutInitiated record exposes schemaVersion and
eventType as constructor parameters despite having fixed constants; add a
compact constructor (or static factory) in FiatPayoutInitiated that ignores
caller-supplied schemaVersion/eventType and enforces schemaVersion =
SCHEMA_VERSION and eventType = EVENT_TYPE (validate or overwrite incoming
values), and apply the same pattern to the sibling event records so nobody can
instantiate these records with mismatched metadata.

Comment on lines +14 to +23
@NotBlank String payoutType,
@NotBlank String stablecoin,
@NotNull @Positive BigDecimal redeemedAmount,
@NotBlank String targetCurrency,
@NotNull @Positive BigDecimal appliedFxRate,
@NotNull UUID recipientId,
@NotBlank String recipientAccountHash,
@NotBlank String paymentRail,
@NotBlank String offRampPartnerId,
@NotBlank String offRampPartnerName

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

Validate enum and length boundaries at the API edge.

These fields only use @NotBlank, but the schema already constrains them tightly in fiat-off-ramp/fiat-off-ramp/src/main/resources/db/migration/V1__initial_schema.sql, Lines 13-28 and Lines 34-39. As written, oversized or unsupported values will pass Bean Validation and fail later at persistence time instead of returning a clean OF-0001 response.

Suggested direction
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
@@
-        `@NotBlank` String payoutType,
-        `@NotBlank` String stablecoin,
+        `@NotBlank`
+        `@Pattern`(regexp = "FIAT|HOLD_STABLECOIN")
+        String payoutType,
+        `@NotBlank`
+        `@Size`(max = 20)
+        String stablecoin,
         `@NotNull` `@Positive` BigDecimal redeemedAmount,
-        `@NotBlank` String targetCurrency,
+        `@NotBlank`
+        `@Pattern`(regexp = "^[A-Z]{3}$")
+        String targetCurrency,
         `@NotNull` `@Positive` BigDecimal appliedFxRate,
         `@NotNull` UUID recipientId,
-        `@NotBlank` String recipientAccountHash,
-        `@NotBlank` String paymentRail,
-        `@NotBlank` String offRampPartnerId,
-        `@NotBlank` String offRampPartnerName
+        `@NotBlank` `@Size`(max = 64) String recipientAccountHash,
+        `@NotBlank` `@Size`(max = 30) String paymentRail,
+        `@NotBlank` `@Size`(max = 50) String offRampPartnerId,
+        `@NotBlank` `@Size`(max = 100) String offRampPartnerName
 ) {}
📝 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
@NotBlank String payoutType,
@NotBlank String stablecoin,
@NotNull @Positive BigDecimal redeemedAmount,
@NotBlank String targetCurrency,
@NotNull @Positive BigDecimal appliedFxRate,
@NotNull UUID recipientId,
@NotBlank String recipientAccountHash,
@NotBlank String paymentRail,
@NotBlank String offRampPartnerId,
@NotBlank String offRampPartnerName
`@NotBlank`
`@Pattern`(regexp = "FIAT|HOLD_STABLECOIN")
String payoutType,
`@NotBlank`
`@Size`(max = 20)
String stablecoin,
`@NotNull` `@Positive` BigDecimal redeemedAmount,
`@NotBlank`
`@Pattern`(regexp = "^[A-Z]{3}$")
String targetCurrency,
`@NotNull` `@Positive` BigDecimal appliedFxRate,
`@NotNull` UUID recipientId,
`@NotBlank` `@Size`(max = 64) String recipientAccountHash,
`@NotBlank` `@Size`(max = 30) String paymentRail,
`@NotBlank` `@Size`(max = 50) String offRampPartnerId,
`@NotBlank` `@Size`(max = 100) String offRampPartnerName
) {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp-api/src/main/java/com/stablecoin/payments/offramp/api/PayoutRequest.java`
around lines 14 - 23, PayoutRequest currently only uses `@NotBlank` for several
tightly constrained fields so invalid lengths or unsupported enum values slip
through to persistence; update the PayoutRequest DTO (fields payoutType,
stablecoin, targetCurrency, recipientAccountHash, paymentRail, offRampPartnerId,
offRampPartnerName) to enforce the same constraints as the DB schema by adding
`@Size`(max=...) with the DB column lengths and use either `@Pattern` or a custom
`@Enum` validator (or javax.validation.Constraint backed by an Enum) for fields
with limited allowed values (e.g., payoutType, stablecoin, paymentRail) so
invalid inputs fail Bean Validation and return the OF-0001 response rather than
failing at persistence.

Comment on lines +13 to +20
@FeignClient(name = "fiat-off-ramp-service", url = "${app.services.fiat-off-ramp.url}")
public interface FiatOffRampClient {

@PostMapping(value = "/v1/payouts", consumes = "application/json", produces = "application/json")
PayoutResponse initiatePayout(@RequestBody PayoutRequest request);

@GetMapping(value = "/v1/payouts/{payoutId}", produces = "application/json")
PayoutResponse getPayout(@PathVariable("payoutId") UUID payoutId);

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

🧩 Analysis chain

🏁 Script executed:

# First, find the service implementation and configuration files
find . -type f -name "application*.yml" -o -name "application*.properties" | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1762


🏁 Script executed:

# Locate the FiatOffRamp service implementation
find . -type f -name "*FiatOffRampService*" -o -name "*FiatOffRampController*" | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Check the repository structure
git ls-files | grep -E "(fiat-off-ramp|application)" | head -30

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3829


🏁 Script executed:

# Check the fiat-off-ramp service configuration
cat ./fiat-off-ramp/fiat-off-ramp/src/main/resources/application.yml

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2888


🏁 Script executed:

# Find the controller in fiat-off-ramp service
find ./fiat-off-ramp/fiat-off-ramp -type f -name "*Controller*" -o -name "*Payout*"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Look for the PayoutResponse and PayoutRequest classes
find . -type f -name "PayoutResponse.java" -o -name "PayoutRequest.java"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 286


🏁 Script executed:

# Find the controller in fiat-off-ramp service with payout endpoints
find ./fiat-off-ramp/fiat-off-ramp/src -type f -name "*.java" | xargs grep -l "@PostMapping.*payouts\|@GetMapping.*payouts" | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Check if there's a PayoutController
find ./fiat-off-ramp/fiat-off-ramp -type f -name "*PayoutController*" -o -name "*Controller.java" | grep -v test

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Search for app.services.fiat-off-ramp.url configuration across the codebase
rg "app\.services\.fiat-off-ramp" --type yaml --type properties -A 2 -B 2

Repository: Puneethkumarck/stablebridge-platform

Length of output: 114


🏁 Script executed:

# Check how the Feign client is used/configured in other services
find . -type f -name "*.yml" -o -name "*.properties" | xargs grep -l "app.services" | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Search for /v1/payouts endpoint mappings in fiat-off-ramp service
rg "/v1/payouts" ./fiat-off-ramp/fiat-off-ramp

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Search for PostMapping or GetMapping annotations in fiat-off-ramp service
rg "@PostMapping|@GetMapping" ./fiat-off-ramp/fiat-off-ramp/src --type java -A 2 | head -50

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Check where FiatOffRampClient is used
rg "FiatOffRampClient" . --type java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 229


🏁 Script executed:

# Check all application.yml files for service endpoint configurations
cat ./fiat-off-ramp/fiat-off-ramp/src/integration-test/resources/application-integration-test.yml

Repository: Puneethkumarck/stablebridge-platform

Length of output: 786


🏁 Script executed:

# Search for RestController in fiat-off-ramp service
find ./fiat-off-ramp/fiat-off-ramp/src/main/java -type f -name "*.java" | xargs grep -l "@RestController"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 206


🏁 Script executed:

# Search for all RequestMapping patterns in fiat-off-ramp service
find ./fiat-off-ramp/fiat-off-ramp/src/main/java -type f -name "*.java" | xargs grep -l "@RequestMapping\|@PostMapping\|@GetMapping"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 64


🏁 Script executed:

# Look for app.services configuration across all yaml files
rg "app:" ./fiat-off-ramp -A 10 --type yaml

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1730


🏁 Script executed:

# Check other service's configuration to understand the pattern
rg "app:" ./merchant-onboarding -A 5 --type yaml | head -50

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1233


🏁 Script executed:

# List all files in fiat-off-ramp service controller directory
find ./fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/controller -type f -name "*.java"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 206


🏁 Script executed:

# Search for all Java controller files
find ./fiat-off-ramp/fiat-off-ramp/src/main -type f -name "*.java" | grep -i controller

Repository: Puneethkumarck/stablebridge-platform

Length of output: 206


🏁 Script executed:

# Look for any endpoint definitions or RequestMapping annotations
rg "@RequestMapping|@GetMapping|@PostMapping" ./fiat-off-ramp/fiat-off-ramp/src/main/java --type java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Check the FiatOffRampClient file again to see the full implementation
cat ./fiat-off-ramp/fiat-off-ramp-client/src/main/java/com/stablecoin/payments/offramp/client/FiatOffRampClient.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1020


🏁 Script executed:

# Search for any payout-related code in the fiat-off-ramp service
rg "payout|Payout" ./fiat-off-ramp/fiat-off-ramp/src/main/java --type java -i

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# List all Java classes in fiat-off-ramp service main source
find ./fiat-off-ramp/fiat-off-ramp/src/main/java -type f -name "*.java" | head -30

Repository: Puneethkumarck/stablebridge-platform

Length of output: 765


🏁 Script executed:

# Check if there's a REST controller elsewhere or if this is a library service
find ./fiat-off-ramp/fiat-off-ramp/src/main -type f -name "*.java" | wc -l

Repository: Puneethkumarck/stablebridge-platform

Length of output: 81


🏁 Script executed:

# Check the GlobalExceptionHandler and other files to understand the service structure
cat ./fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/application/controller/GlobalExceptionHandler.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3113


🏁 Script executed:

# Check the main application file
cat ./fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/FiatOffRampApplication.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 701


🏁 Script executed:

# Search across entire codebase for where app.services.fiat-off-ramp.url might be defined
rg "app\.services\.fiat-off-ramp" . --type yaml

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Check other services to see how they configure app.services URLs
rg "app\.services\." ./api-gateway-iam/api-gateway-iam/src/main/resources --type yaml -B 2 -A 2

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

# Double-check by searching for any REST endpoints or payout service implementations elsewhere
find . -path ./node_modules -prune -o -type f -name "*.java" -exec grep -l "PayoutResponse initiatePayout\|PayoutResponse getPayout" {} \;

Repository: Puneethkumarck/stablebridge-platform

Length of output: 192


🏁 Script executed:

# Check if there are integration tests that show expected behavior
find ./fiat-off-ramp -name "*Test*.java" -o -name "*IT.java" | xargs grep -l "payout\|Payout" -i 2>/dev/null | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 196


🏁 Script executed:

# Check the integration test
cat ./fiat-off-ramp/fiat-off-ramp/src/integration-test/java/com/stablecoin/payments/offramp/AbstractIntegrationTest.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2321


🏁 Script executed:

# Search for any controller or rest endpoint implementations that might handle payout requests
rg "@PostMapping|@GetMapping|@RequestMapping" ./fiat-off-ramp -B 2 -A 5 --type java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1349


🏁 Script executed:

# Check the PR or documentation for expected API structure
find ./fiat-off-ramp -name "README*" -o -name "*.md" | xargs cat 2>/dev/null

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


Add context path to the Feign client configuration.

The service runs under /off-ramp context path (configured in server.servlet.context-path), but the Feign client targets /v1/payouts directly. When service endpoints are implemented, requests will hit http://host:port/v1/payouts instead of http://host:port/off-ramp/v1/payouts, resulting in 404s. Use the path attribute to establish the correct base path at the client level.

Proposed fix
-@FeignClient(name = "fiat-off-ramp-service", url = "${app.services.fiat-off-ramp.url}")
+@FeignClient(
+        name = "fiat-off-ramp-service",
+        url = "${app.services.fiat-off-ramp.url}",
+        path = "/off-ramp"
+)
 public interface FiatOffRampClient {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp-client/src/main/java/com/stablecoin/payments/offramp/client/FiatOffRampClient.java`
around lines 13 - 20, The Feign client FiatOffRampClient is missing the service
context path so requests to initiatePayout and getPayout will target /v1/...
instead of /off-ramp/v1/.... Update the `@FeignClient` annotation (on interface
FiatOffRampClient) to include the path attribute for the service context (e.g.,
path="/off-ramp") so the client-level base path is applied and both
initiatePayout and getPayout resolve to /off-ramp/v1/payouts.

Comment on lines +25 to +26
compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output

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

Keep src/test output off the higher-level test classpaths.

Both custom source sets compile against sourceSets.test.output. That lets integration/business suites depend on unit-test-only helpers and mocks instead of the fixture API, which makes the suite boundaries brittle. Shared helpers should come from testFixtures(project) only.

Proposed fix
 val integrationTestSourceSet: SourceSet = sourceSets.create("integrationTest") {
     java.srcDir("src/integration-test/java")
     resources.srcDir("src/integration-test/resources")
-    compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output
-    runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output
+    compileClasspath += sourceSets.main.get().output
+    runtimeClasspath += sourceSets.main.get().output
 }
@@
 val businessTestSourceSet: SourceSet = sourceSets.create("businessTest") {
     java.srcDir("src/business-test/java")
     resources.srcDir("src/business-test/resources")
-    compileClasspath += sourceSets.main.get().output + sourceSets.test.get().output + integrationTestSourceSet.output
-    runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().output + integrationTestSourceSet.output
+    compileClasspath += sourceSets.main.get().output + integrationTestSourceSet.output
+    runtimeClasspath += sourceSets.main.get().output + integrationTestSourceSet.output
 }

Also applies to: 46-47

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

In `@fiat-off-ramp/fiat-off-ramp/build.gradle.kts` around lines 25 - 26, The build
file is incorrectly adding sourceSets.test.get().output to higher-level
classpaths (compileClasspath and runtimeClasspath), which exposes unit-test-only
helpers to integration/business suites; remove the "+
sourceSets.test.get().output" from the compileClasspath and runtimeClasspath
assignments in the blocks that modify those classpaths (and the similar
occurrences around the 46-47 region), leaving only sourceSets.main.get().output,
and instead add explicit testFixtures dependencies where shared test helpers are
needed (use testFixtures(project) or implementation(testFixtures(project)) on
the appropriate source sets) so shared fixtures come from the testFixtures API
rather than src/test output.

Comment on lines +117 to +119
app:
security:
enabled: false

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for production profile overrides
echo "=== Checking for production/deployment configs ==="
fd -e yml -e yaml | xargs grep -l "security" 2>/dev/null | head -10
fd -e yml -e yaml --exec grep -A2 "app:" {} \; 2>/dev/null

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3357


🏁 Script executed:

#!/bin/bash
# Check fiat-off-ramp application configs specifically
echo "=== Fiat-off-ramp application configs ==="
fd "application" fiat-off-ramp -e yml -e yaml

echo ""
echo "=== Content of fiat-off-ramp application.yml (lines 110-125) ==="
sed -n '110,125p' fiat-off-ramp/fiat-off-ramp/src/main/resources/application.yml

echo ""
echo "=== Check for prod/production configs ==="
fd "application" fiat-off-ramp -e yml -e yaml | grep -i prod

echo ""
echo "=== All security-related configs in fiat-off-ramp ==="
rg "security" fiat-off-ramp/fiat-off-ramp/src/main/resources/

Repository: Puneethkumarck/stablebridge-platform

Length of output: 929


🏁 Script executed:

#!/bin/bash
echo "=== Checking for fiat-off-ramp deployment configs ==="
find . -name "*fiat*off*ramp*" -o -name "fiat-off-ramp" | grep -E "(k8s|deploy|docker|helm)" | head -20

echo ""
echo "=== Check docker-compose.dev.yml for fiat-off-ramp ==="
rg "fiat-off-ramp|fiat_off_ramp" docker-compose.dev.yml -A 10 -i

echo ""
echo "=== Check for any Kubernetes deployment manifests ==="
find . -path "*/fiat-off-ramp/*" -name "*.yml" -o -name "*.yaml" | grep -v "/src/"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1122


Security disabled by default — adopt environment variable pattern with safe fallback.

app.security.enabled: false is hardcoded without an environment variable override mechanism. Update to match the pattern used elsewhere in the codebase (e.g., s13):

app:
  security:
    enabled: ${APP_SECURITY_ENABLED:true}

This ensures security is enabled by default and explicitly disabled only when configured at deployment time. Also create an application-prod.yml profile to enforce this in production environments.

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

In `@fiat-off-ramp/fiat-off-ramp/src/main/resources/application.yml` around lines
117 - 119, Replace the hardcoded property app.security.enabled: false with an
environment-backed property so the default is secure and can be overridden;
change the value to reference ${APP_SECURITY_ENABLED:true} (property name
APP_SECURITY_ENABLED) in the main application.yml and add an
application-prod.yml profile that explicitly sets app.security.enabled: true to
enforce security in production. Ensure the property key app.security.enabled is
used consistently across config and any code that reads it.

Comment on lines +107 to +108
CREATE INDEX idx_offramp_outbox_record_status
ON offramp_outbox_record (status, next_retry_at);

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

Align this index name with the migration's _idx convention.

Every other new index in this file uses the <table>_<columns>_idx pattern; idx_offramp_outbox_record_status is the outlier. Rename it before merge so V1 establishes the convention cleanly.

As per coding guidelines, **/db/migration/**: - Verify index naming follows the project convention.

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

In
`@fiat-off-ramp/fiat-off-ramp/src/main/resources/db/migration/V1__initial_schema.sql`
around lines 107 - 108, Rename the index idx_offramp_outbox_record_status to
follow the <table>_<columns>_idx convention by creating it as
offramp_outbox_record_status_next_retry_at_idx on table offramp_outbox_record
for columns (status, next_retry_at); update the CREATE INDEX statement
accordingly and ensure any references (drops or constraints) use the new name so
V1 uses the consistent naming convention.

Comment on lines +28 to +37
@DisplayName("Domain should not depend on Spring (except stereotype and transaction)")
void domainShouldNotDependOnSpring() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat(
resideInAPackage("org.springframework..")
.and(resideOutsideOfPackage("org.springframework.stereotype.."))
.and(resideOutsideOfPackage("org.springframework.transaction.."))
.and(resideOutsideOfPackage("org.springframework.beans.factory.annotation.."))
)

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

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "ArchitectureTest.java" | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 625


🏁 Script executed:

cat -n fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java | head -60

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2993


🏁 Script executed:

cat -n fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java | sed -n '20,50p'

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1640


🏁 Script executed:

find . -path "*/domain/*" -name "*.java" -type f | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2626


🏁 Script executed:

rg "@Autowired|@Value|@Inject" fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/domain/ --type java -B 2

Repository: Puneethkumarck/stablebridge-platform

Length of output: 201


🏁 Script executed:

rg "org.springframework.beans.factory.annotation" fiat-off-ramp/fiat-off-ramp/src/main --type java -l

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp/src/main/java -type d | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 721


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp/src/main/java -name "*omain*" -type d

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

rg "@Autowired|@Value|@Inject" fiat-off-ramp/ --type java -l | grep -v test

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp/src/main/java -type f -name "*.java" | head -30

Repository: Puneethkumarck/stablebridge-platform

Length of output: 753


🏁 Script executed:

ls -la fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/

Repository: Puneethkumarck/stablebridge-platform

Length of output: 379


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp -type d -name "*" | grep -v ".class" | sort

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2371


🏁 Script executed:

cat fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 4415


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp/src/main -type d | grep -i domain

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

git log --oneline -S "beans.factory.annotation" -- fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 140


🏁 Script executed:

git show --stat $(git log --oneline -S "beans.factory.annotation" -- fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java | head -1 | cut -d' ' -f1)

Repository: Puneethkumarck/stablebridge-platform

Length of output: 50392


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp/src/main/java -type f -name "*.java" | xargs cat | grep -E "@Autowired|@Value|@Inject|@Bean" | head -20

Repository: Puneethkumarck/stablebridge-platform

Length of output: 109


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp -type d -name "domain"

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

ls -la fiat-off-ramp/fiat-off-ramp/src/main/java/com/stablecoin/payments/offramp/ | grep -i domain

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

grep -r "..domain.." fiat-off-ramp/fiat-off-ramp/src --include="*.java" | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1725


🏁 Script executed:

find fiat-off-ramp/fiat-off-ramp/src/main/java -type f -name "*.java" | xargs grep -l "package.*domain" | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

find . -path "*offramp*" -name "*.java" | xargs grep "package com.stablecoin.payments.offramp" | grep -v test | cut -d: -f1 | sort -u

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1643


🏁 Script executed:

cat -n fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java

Repository: Puneethkumarck/stablebridge-platform

Length of output: 5199


🏁 Script executed:

python3 << 'EOF'
# Check if there's any domain package in fiat-off-ramp
import os
import subprocess

result = subprocess.run(
    ["find", "fiat-off-ramp/fiat-off-ramp/src/main/java", "-type", "d", "-name", "*domain*"],
    capture_output=True,
    text=True
)
print("Domain directories found:")
print(result.stdout)
print("stderr:", result.stderr)

# Also check test output for domain packages
result2 = subprocess.run(
    ["find", "fiat-off-ramp/fiat-off-ramp", "-type", "f", "-name", "*.java"],
    capture_output=True,
    text=True
)
files = result2.stdout.strip().split('\n')
domain_files = [f for f in files if 'domain' in f.lower()]
print("\nFiles with 'domain' in path:")
for f in domain_files[:20]:
    print(f)
EOF

Repository: Puneethkumarck/stablebridge-platform

Length of output: 145


🏁 Script executed:

find . -path "*/src/main/java/*" -type d -name "domain" | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 832


🏁 Script executed:

cat api-gateway-iam/api-gateway-iam/src/test/java/com/stablecoin/payments/gateway/iam/ArchitectureTest.java | sed -n '25,45p'

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1051


🏁 Script executed:

find api-gateway-iam/api-gateway-iam/src/main/java/com/stablecoin/payments/gateway/iam/domain -name "*.java" | xargs grep -l "@Autowired\|@Value\|@Inject" | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 200


🏁 Script executed:

grep -r "org.springframework.beans.factory.annotation" . --include="*.java" | grep -v test | head -10

Repository: Puneethkumarck/stablebridge-platform

Length of output: 1241


🏁 Script executed:

cat api-gateway-iam/api-gateway-iam/src/main/java/com/stablecoin/payments/gateway/iam/domain/service/AuthCommandHandler.java | head -40

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2162


Update the display name to accurately document all Spring exemptions.

The domainShouldNotDependOnSpring rule permits three Spring packages but the display name only mentions stereotype and transaction. Line 36 also exempts org.springframework.beans.factory.annotation.., which is necessary since domain services across the codebase (e.g., api-gateway-iam's AuthCommandHandler) use @Value for configuration injection. Update the @DisplayName on line 28 to reflect all three exemptions:

`@DisplayName`("Domain should not depend on Spring (except stereotype, transaction, and beans.factory.annotation)")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/arch/ArchitectureTest.java`
around lines 28 - 37, Update the test display name in ArchitectureTest for the
domainShouldNotDependOnSpring rule to list all three Spring exemptions; change
the `@DisplayName` on the domainShouldNotDependOnSpring test to explicitly mention
"stereotype, transaction, and beans.factory.annotation" so it matches the
resideOutsideOfPackage exemptions (org.springframework.stereotype..,
org.springframework.transaction..,
org.springframework.beans.factory.annotation..).

Comment on lines +24 to +33
try {
assertThat(original)
.usingRecursiveComparison()
.ignoringFieldsOfTypes(ZonedDateTime.class, LocalDateTime.class, LocalDate.class, Instant.class)
.ignoringFields(fieldsToIgnore)
.isEqualTo(expected);
return true;
} catch (Throwable t) {
return false;
}

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

Do not swallow Throwable in the matcher helper.

Only the AssertJ comparison failure needs to turn into false. Catching Throwable will also hide OutOfMemoryError, LinkageError, and similar failures as a silent matcher miss, which makes test failures much harder to diagnose.

Suggested fix
         try {
             assertThat(original)
                     .usingRecursiveComparison()
                     .ignoringFieldsOfTypes(ZonedDateTime.class, LocalDateTime.class, LocalDate.class, Instant.class)
                     .ignoringFields(fieldsToIgnore)
                     .isEqualTo(expected);
             return true;
-        } catch (Throwable t) {
+        } catch (AssertionError ignored) {
             return false;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/TestUtils.java`
around lines 24 - 33, The helper in TestUtils currently catches Throwable and
hides serious errors; change the catch to only catch AssertionError (the
AssertJ/JUnit comparison failure) and return false there, and let all other
Throwables/Errors propagate (i.e., rethrow or don't catch them). Locate the
matcher helper in TestUtils.java that compares original and expected (it
references original, expected, fieldsToIgnore, and uses
usingRecursiveComparison()) and replace the catch(Throwable t) block with a
catch(AssertionError ae) { return false; } so only assertion failures become
false and other errors are not swallowed.

Bootstrap S5 Fiat Off-Ramp with 3 Gradle modules (api, client, app),
Flyway V1 (6 tables: payout_orders, stablecoin_redemptions,
off_ramp_transactions, 3 Namastack outbox), SecurityConfig,
IdempotencyKeyFilter, CorrelationIdFilter, GlobalExceptionHandler,
8 ArchUnit tests, AbstractIntegrationTest, and TestUtils.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck force-pushed the feature/STA-146-s5-fiat-off-ramp-scaffold branch from 2a5738b to 725e06a Compare March 9, 2026 22:10
Puneethkumarck and others added 2 commits March 9, 2026 23:16
- Remove duplicate payment_id index (UNIQUE constraint already creates one)
- Include offramp_outbox_instance and offramp_outbox_partition in test truncation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redundant with CodeRabbit reviews. Removes CI noise and rate limit issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck merged commit 132d0e5 into main Mar 9, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature phase-3 Value Movement MVP service-s5 Off-Ramp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant