Skip to content

feat(s3): Stripe PSP adapter + WireMock stubs (STA-124)#127

Merged
Puneethkumarck merged 1 commit into
mainfrom
feature/STA-124-stripe-psp-adapter
Mar 8, 2026
Merged

feat(s3): Stripe PSP adapter + WireMock stubs (STA-124)#127
Puneethkumarck merged 1 commit into
mainfrom
feature/STA-124-stripe-psp-adapter

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements StripePspAdapter as ACL adapter for PspGateway domain port — form-encoded requests to Stripe API (PaymentIntents + Refunds)
  • StripeProperties with @ConfigurationProperties(prefix = "app.psp.stripe") — configurable base URL, API key, timeout
  • Package-private ACL DTOs (StripePaymentIntentResponse, StripeRefundResponse) with Long wrapper types (Jackson 3 compat)
  • @CircuitBreaker(name = "stripe") on both methods with hard-block fallbacks (re-throw)
  • RestClient with HttpClient.Version.HTTP_1_1 (avoids WireMock EOF errors)
  • Fallback PspGateway bean in FallbackAdaptersConfig via @ConditionalOnMissingBean
  • 7 WireMock-based unit tests (success, error, timeout, request body verification for both payment + refund)

Test plan

  • 7 new WireMock unit tests all pass
  • All 221 S3 unit tests green (214 existing + 7 new)
  • Spotless formatting clean

Closes STA-124

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated Stripe as a payment service provider with support for payment initiation and refund processing
    • Added fallback payment gateway mechanism to ensure consistent operation
  • Configuration

    • Added Stripe configuration options with customizable base URL, API authentication, and timeout settings
  • Tests

    • Added comprehensive test coverage for Stripe operations, including success cases, error scenarios, and timeout handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown

Walkthrough

This pull request introduces Stripe as a Payment Service Provider (PSP) gateway with payment initiation and refund capabilities. Includes configuration properties with defaults, an HTTP-based adapter with circuit breaker protection, fallback bean for development environments, and comprehensive test coverage using WireMock to simulate Stripe API interactions.

Changes

Cohort / File(s) Summary
Domain Models
StripePaymentIntentResponse.java, StripeRefundResponse.java
New records defining Stripe API response structures with id, status, amount, currency, and optional fields.
Configuration
StripeProperties.java, application.yml
Stripe-specific configuration properties with built-in validation and defaults (baseUrl, apiKey, timeoutSeconds). YAML configuration wires environment-based API key with fallback.
Stripe Adapter Implementation
StripePspAdapter.java
Spring component implementing PspGateway; handles payment and refund initiation via REST calls to Stripe API. Includes HTTP/1.1 client setup, form-encoded payloads with amount conversion to minor units, circuit breaker with exception-throwing fallback, and lifecycle logging.
Fallback Configuration
FallbackAdaptersConfig.java
Adds conditional bean providing synthetic PSP gateway for dev/test environments; returns mocked PspPaymentResult and PspRefundResult with UUID-based IDs and "succeeded" status.
Test Suite
StripePspAdapterTest.java
Comprehensive WireMock-based tests covering payment/refund success paths, Stripe error responses (402, 404), timeout scenarios, and request payload validation for form parameters and metadata.

Sequence Diagram

sequenceDiagram
    participant Client
    participant StripePspAdapter
    participant CircuitBreaker
    participant RestClient
    participant StripeAPI as Stripe API
    participant Fallback

    Client->>StripePspAdapter: initiatePayment(request)
    StripePspAdapter->>StripePspAdapter: convert amount to minor units
    StripePspAdapter->>StripePspAdapter: build form-encoded payload
    StripePspAdapter->>CircuitBreaker: execute payment call
    
    alt Circuit Open or Error
        CircuitBreaker->>Fallback: invoke fallback
        Fallback-->>CircuitBreaker: throw IllegalStateException
        CircuitBreaker-->>StripePspAdapter: propagate exception
    else Success Path
        CircuitBreaker->>RestClient: POST /v1/payment_intents
        RestClient->>StripeAPI: HTTP POST (baseUrl + apiKey)
        StripeAPI-->>RestClient: StripePaymentIntentResponse
        RestClient-->>CircuitBreaker: response
        CircuitBreaker-->>StripePspAdapter: parsed result
    end
    
    StripePspAdapter->>StripePspAdapter: log lifecycle event
    StripePspAdapter-->>Client: PspPaymentResult (id, status)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

phase-3, service-s3, feature

🚥 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 Title accurately describes the main change: implementing Stripe PSP adapter with WireMock tests, directly matching the PR's core deliverables.
Description check ✅ Passed Description covers all required template sections: summary, related issue (STA-124), type of change (feature), key changes, testing approach, and security considerations. All checklist items addressed.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/STA-124-stripe-psp-adapter

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 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-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/config/FallbackAdaptersConfig.java`:
- Around line 26-42: Replace the fully-qualified parameter types in the
anonymous PspGateway implementation with the imported types PspPaymentRequest
and PspRefundRequest: add imports for
com.stablecoin.payments.onramp.domain.port.PspPaymentRequest and
com.stablecoin.payments.onramp.domain.port.PspRefundRequest at the top of the
file, then change the method signatures of initiatePayment(...) and
initiateRefund(...) to use PspPaymentRequest and PspRefundRequest respectively
instead of the FQCNs; keep the rest of the PspGateway implementation unchanged.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePaymentIntentResponse.java`:
- Around line 3-10: The record StripePaymentIntentResponse maps Stripe's JSON
but misses the client_secret mapping; annotate the clientSecret component with
`@JsonProperty`("client_secret") in the StripePaymentIntentResponse record and add
the necessary import (com.fasterxml.jackson.annotation.JsonProperty) so Jackson
correctly binds Stripe's client_secret to the clientSecret field.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeProperties.java`:
- Around line 8-18: The StripeProperties constructor silently falls back to a
test API key; change this to fail fast by adding validation: annotate the
properties class with `@Validated` and mark the apiKey field or constructor
parameter with `@NotBlank` (remove the silent default assignment of
"sk_test_default"), so Spring will fail startup when apiKey is missing; if you
prefer a softer change, keep the fallback but emit a warning via the logger in
the StripeProperties constructor when apiKey is null/blank instead of silently
assigning "sk_test_default".

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java`:
- Around line 80-84: The refund reason is being hardcoded in StripePspAdapter
(formData.add("reason", "requested_by_customer")) instead of using the
PspRefundRequest.reason; update StripePspAdapter to map
PspRefundRequest.reason() to Stripe's accepted values (e.g., "duplicate",
"fraudulent", "requested_by_customer") and pass that mapped value into the
formData "reason" field; validate/normalize the incoming reason (or use a safe
default like "requested_by_customer") and keep other fields (payment_intent,
amount via toMinorUnitsFromRefund, metadata[collection_id]) unchanged.
- Around line 61-72: The current StripePspAdapter code assumes the
StripePaymentIntentResponse (response) is non-null after
restClient.post()...retrieve().body(StripePaymentIntentResponse.class); add a
defensive null-check for the response returned by that call (e.g., immediately
after the body(...) call), and handle the null case by logging an error via
log.error with context (collectionId) and either throwing a clear exception or
returning a failure PspPaymentResult, rather than dereferencing
response.id()/response.status(); update the log.info and the returned
PspPaymentResult usage to occur only when response != null.
- Around line 113-119: The conversion to minor units in
toMinorUnits(PspPaymentRequest) and toMinorUnitsFromRefund(PspRefundRequest)
incorrectly hardcodes movePointRight(2); instead, read the currency via
request.amount().currency() and request.refundAmount().currency(), determine the
correct exponent for that currency (0 for Stripe zero-decimal currencies like
JPY, KRW, etc., otherwise typically 2, or consult a currency-to-decimal map),
then multiply by 10^exponent (e.g. movePointRight(exponent)) and call
longValueExact() to produce the minor-unit string; implement a small helper or
map inside StripePspAdapter to return the exponent for a given currency code and
reuse it from both methods.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeRefundResponse.java`:
- Around line 3-10: Stripe API returns snake_case fields but the record fields
are camelCase, so annotate the record components with `@JsonProperty` to map JSON
names to Java names: add import com.fasterxml.jackson.annotation.JsonProperty
and annotate StripeRefundResponse(String id, String status, Long amount, String
currency, `@JsonProperty`("payment_intent") String paymentIntent) and similarly
update StripePaymentIntentResponse to annotate the clientSecret component with
`@JsonProperty`("client_secret"); this ensures Jackson binds payment_intent and
client_secret into paymentIntent and clientSecret respectively.

In
`@fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java`:
- Around line 145-147: The test in StripePspAdapterTest is asserting any
Exception for timeoutAdapter.initiatePayment(aPaymentRequest()), which is too
broad; change the assertion to expect the specific timeout exception (e.g.,
java.net.http.HttpReadTimeoutException or the Spring wrapper such as
org.springframework.web.client.ResourceAccessException caused by a timeout) by
replacing isInstanceOf(Exception.class) with an assertion that targets the
concrete exception type (or the wrapper and its cause) so the test only passes
on real timeout scenarios and references timeoutAdapter.initiatePayment and
aPaymentRequest() when updated.
- Around line 120-122: The current test in StripePspAdapterTest uses a too-broad
assertion (.isInstanceOf(Exception.class)) for
adapter.initiatePayment(aPaymentRequest()); change this to assert a specific
exception type thrown by the HTTP client used in the adapter (for example
replace .isInstanceOf(Exception.class) with
.isInstanceOf(WebClientResponseException.class) or
.isInstanceOf(HttpClientErrorException.class) as appropriate), and optionally
tighten the assertion by checking status/message (e.g.,
.hasMessageContaining(...) or .satisfies(e ->
((WebClientResponseException)e).getStatusCode() == HttpStatus.BAD_REQUEST)) to
ensure meaningful failure checks for initiatePayment.
- Around line 220-222: The test StripePspAdapterTest currently asserts a generic
Exception for the 404 refund path; update the assertion to expect the concrete
exception type that StripePspAdapter.initiateRefund throws for HTTP 404 (for
example NotFoundException, PspResourceNotFoundException, or the specific
StripeApiException used), i.e. replace isInstanceOf(Exception.class) with
isInstanceOf(TheConcrete404Exception.class) and optionally assert the message or
status code to ensure it’s the 404 case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5930c057-4bf9-47e5-af74-104845b3cfd6

📥 Commits

Reviewing files that changed from the base of the PR and between 5438a74 and 8176c58.

📒 Files selected for processing (7)
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/config/FallbackAdaptersConfig.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePaymentIntentResponse.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeProperties.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeRefundResponse.java
  • fiat-on-ramp/fiat-on-ramp/src/main/resources/application.yml
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java

Comment on lines +26 to +42
return new PspGateway() {
@Override
public PspPaymentResult initiatePayment(
com.stablecoin.payments.onramp.domain.port.PspPaymentRequest request) {
log.warn("[FALLBACK-PSP] Using dev PSP gateway for payment collectionId={}",
request.collectionId());
return new PspPaymentResult("dev-pi-" + UUID.randomUUID(), "succeeded");
}

@Override
public PspRefundResult initiateRefund(
com.stablecoin.payments.onramp.domain.port.PspRefundRequest request) {
log.warn("[FALLBACK-PSP] Using dev PSP gateway for refund collectionId={}",
request.collectionId());
return new PspRefundResult("dev-re-" + UUID.randomUUID(), "succeeded");
}
};

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

Use imported types instead of fully-qualified class names.

PspPaymentRequest and PspRefundRequest should be imported at the top of the file rather than using FQCNs inline.

Proposed fix

Add to imports:

import com.stablecoin.payments.onramp.domain.port.PspPaymentRequest;
import com.stablecoin.payments.onramp.domain.port.PspRefundRequest;

Then update the method signatures:

             `@Override`
-            public PspPaymentResult initiatePayment(
-                    com.stablecoin.payments.onramp.domain.port.PspPaymentRequest request) {
+            public PspPaymentResult initiatePayment(PspPaymentRequest request) {
                 log.warn("[FALLBACK-PSP] Using dev PSP gateway for payment collectionId={}",
                         request.collectionId());
                 return new PspPaymentResult("dev-pi-" + UUID.randomUUID(), "succeeded");
             }

             `@Override`
-            public PspRefundResult initiateRefund(
-                    com.stablecoin.payments.onramp.domain.port.PspRefundRequest request) {
+            public PspRefundResult initiateRefund(PspRefundRequest request) {
                 log.warn("[FALLBACK-PSP] Using dev PSP gateway for refund collectionId={}",
                         request.collectionId());
                 return new PspRefundResult("dev-re-" + UUID.randomUUID(), "succeeded");
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/config/FallbackAdaptersConfig.java`
around lines 26 - 42, Replace the fully-qualified parameter types in the
anonymous PspGateway implementation with the imported types PspPaymentRequest
and PspRefundRequest: add imports for
com.stablecoin.payments.onramp.domain.port.PspPaymentRequest and
com.stablecoin.payments.onramp.domain.port.PspRefundRequest at the top of the
file, then change the method signatures of initiatePayment(...) and
initiateRefund(...) to use PspPaymentRequest and PspRefundRequest respectively
instead of the FQCNs; keep the rest of the PspGateway implementation unchanged.

Comment on lines +3 to +10
record StripePaymentIntentResponse(
String id,
String status,
Long amount,
String currency,
String clientSecret
) {
}

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

Missing @JsonProperty for client_secret mapping.

Same issue as StripeRefundResponse — Stripe returns client_secret, not clientSecret.

Proposed fix
 package com.stablecoin.payments.onramp.infrastructure.provider.stripe;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 record StripePaymentIntentResponse(
         String id,
         String status,
         Long amount,
         String currency,
-        String clientSecret
+        `@JsonProperty`("client_secret") String clientSecret
 ) {
 }
📝 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
record StripePaymentIntentResponse(
String id,
String status,
Long amount,
String currency,
String clientSecret
) {
}
record StripePaymentIntentResponse(
String id,
String status,
Long amount,
String currency,
`@JsonProperty`("client_secret") String clientSecret
) {
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePaymentIntentResponse.java`
around lines 3 - 10, The record StripePaymentIntentResponse maps Stripe's JSON
but misses the client_secret mapping; annotate the clientSecret component with
`@JsonProperty`("client_secret") in the StripePaymentIntentResponse record and add
the necessary import (com.fasterxml.jackson.annotation.JsonProperty) so Jackson
correctly binds Stripe's client_secret to the clientSecret field.

Comment on lines +8 to +18
public StripeProperties {
if (baseUrl == null || baseUrl.isBlank()) {
baseUrl = "https://api.stripe.com";
}
if (apiKey == null || apiKey.isBlank()) {
apiKey = "sk_test_default";
}
if (timeoutSeconds <= 0) {
timeoutSeconds = 10;
}
}

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

Silent fallback to default API key may mask production misconfiguration.

Defaulting apiKey to sk_test_default when blank could cause production issues if the environment variable isn't set — requests would silently fail authentication against Stripe.

Consider using @Validated with @NotBlank on apiKey to fail fast, or at minimum log a warning when falling back.

Alternative: fail-fast validation
 package com.stablecoin.payments.onramp.infrastructure.provider.stripe;
 
+import jakarta.validation.constraints.NotBlank;
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
 
+@Validated
 `@ConfigurationProperties`(prefix = "app.psp.stripe")
-public record StripeProperties(String baseUrl, String apiKey, int timeoutSeconds) {
+public record StripeProperties(
+        String baseUrl,
+        `@NotBlank`(message = "Stripe API key must be configured") String apiKey,
+        int timeoutSeconds) {
 
     public StripeProperties {
         if (baseUrl == null || baseUrl.isBlank()) {
             baseUrl = "https://api.stripe.com";
         }
-        if (apiKey == null || apiKey.isBlank()) {
-            apiKey = "sk_test_default";
-        }
         if (timeoutSeconds <= 0) {
             timeoutSeconds = 10;
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeProperties.java`
around lines 8 - 18, The StripeProperties constructor silently falls back to a
test API key; change this to fail fast by adding validation: annotate the
properties class with `@Validated` and mark the apiKey field or constructor
parameter with `@NotBlank` (remove the silent default assignment of
"sk_test_default"), so Spring will fail startup when apiKey is missing; if you
prefer a softer change, keep the fallback but emit a warning via the logger in
the StripeProperties constructor when apiKey is null/blank instead of silently
assigning "sk_test_default".

Comment on lines +61 to +72
var response = restClient.post()
.uri("/v1/payment_intents")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(formData)
.retrieve()
.body(StripePaymentIntentResponse.class);

log.info("[STRIPE] Payment initiated collectionId={} pspRef={} status={}",
request.collectionId(), response.id(), response.status());

return new PspPaymentResult(response.id(), response.status());
}

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

Potential NPE if response is null.

retrieve().body(...) can return null if the response body is empty. While RestClient typically throws on non-2xx statuses, defensive null handling would be safer.

Defensive approach
         var response = restClient.post()
                 .uri("/v1/payment_intents")
                 .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                 .body(formData)
                 .retrieve()
                 .body(StripePaymentIntentResponse.class);
 
+        if (response == null || response.id() == null) {
+            throw new IllegalStateException("Empty response from Stripe payment intent API");
+        }
+
         log.info("[STRIPE] Payment initiated collectionId={} pspRef={} status={}",
                 request.collectionId(), response.id(), response.status());
📝 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
var response = restClient.post()
.uri("/v1/payment_intents")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(formData)
.retrieve()
.body(StripePaymentIntentResponse.class);
log.info("[STRIPE] Payment initiated collectionId={} pspRef={} status={}",
request.collectionId(), response.id(), response.status());
return new PspPaymentResult(response.id(), response.status());
}
var response = restClient.post()
.uri("/v1/payment_intents")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(formData)
.retrieve()
.body(StripePaymentIntentResponse.class);
if (response == null || response.id() == null) {
throw new IllegalStateException("Empty response from Stripe payment intent API");
}
log.info("[STRIPE] Payment initiated collectionId={} pspRef={} status={}",
request.collectionId(), response.id(), response.status());
return new PspPaymentResult(response.id(), response.status());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java`
around lines 61 - 72, The current StripePspAdapter code assumes the
StripePaymentIntentResponse (response) is non-null after
restClient.post()...retrieve().body(StripePaymentIntentResponse.class); add a
defensive null-check for the response returned by that call (e.g., immediately
after the body(...) call), and handle the null case by logging an error via
log.error with context (collectionId) and either throwing a clear exception or
returning a failure PspPaymentResult, rather than dereferencing
response.id()/response.status(); update the log.info and the returned
PspPaymentResult usage to occur only when response != null.

Comment on lines +80 to +84
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("payment_intent", request.pspReference());
formData.add("amount", toMinorUnitsFromRefund(request));
formData.add("reason", "requested_by_customer");
formData.add("metadata[collection_id]", request.collectionId().toString());

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

Refund reason from request is ignored.

PspRefundRequest includes a reason field (passed as "customer_request" in tests), but it's hardcoded to "requested_by_customer" here. Consider mapping the request's reason to Stripe's valid enum values.

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

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java`
around lines 80 - 84, The refund reason is being hardcoded in StripePspAdapter
(formData.add("reason", "requested_by_customer")) instead of using the
PspRefundRequest.reason; update StripePspAdapter to map
PspRefundRequest.reason() to Stripe's accepted values (e.g., "duplicate",
"fraudulent", "requested_by_customer") and pass that mapped value into the
formData "reason" field; validate/normalize the incoming reason (or use a safe
default like "requested_by_customer") and keep other fields (payment_intent,
amount via toMinorUnitsFromRefund, metadata[collection_id]) unchanged.

Comment on lines +113 to +119
private String toMinorUnits(PspPaymentRequest request) {
return String.valueOf(request.amount().amount().movePointRight(2).longValueExact());
}

private String toMinorUnitsFromRefund(PspRefundRequest request) {
return String.valueOf(request.refundAmount().amount().movePointRight(2).longValueExact());
}

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

🌐 Web query:

Stripe API zero decimal currencies list

💡 Result:

Stripe “zero-decimal currencies” (currencies where you don’t multiply by 100—the amount is already in the smallest unit) are: BIF, CLP, DJF, GNF, JPY, KMF, KRW, MGA, PYG, RWF, UGX, VND, VUV, XAF, XOF, XPF. [1][2][3]

Source references: Stripe’s general rule for zero-decimal currencies is documented in their “Supported currencies” guide (even if the list rendering can vary by page/view). [1]
Notes: Stripe also documents some special cases (for example ISK and UGX backward-compatibility behavior, and payout handling for HUF/TWD). [1]

References:
[1] https://docs.stripe.com/currencies
[2] stripe/stripe-java#874
[3] https://growpanel.io/docs/integrations/stripe/


🏁 Script executed:

cat -n fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java | head -150

Repository: Puneethkumarck/stablebridge-platform

Length of output: 6491


🏁 Script executed:

find . -name "*.java" -type f | xargs grep -l "interface PspRefundRequest\|class PspRefundRequest" 2>/dev/null | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 62


🏁 Script executed:

rg "PspRefundRequest" --type java -A 10 | head -50

Repository: Puneethkumarck/stablebridge-platform

Length of output: 7001


🏁 Script executed:

rg "class Money|record Money" --type java -A 5 | head -30

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3926


Minor units conversion fails for zero-decimal and special-decimal currencies.

movePointRight(2) hardcodes a 2-decimal assumption incompatible with Stripe's payment calculation rules. Zero-decimal currencies (JPY, KRW, BIF, CLP, DJF, GNF, KMF, MGA, PYG, RWF, UGX, VND, VUV, XAF, XOF, XPF) require no multiplication; others may differ. Currency data is available via request.amount().currency() and request.refundAmount().currency(), so apply appropriate conversion per currency.

Both toMinorUnits() and toMinorUnitsFromRefund() require fixing.

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

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.java`
around lines 113 - 119, The conversion to minor units in
toMinorUnits(PspPaymentRequest) and toMinorUnitsFromRefund(PspRefundRequest)
incorrectly hardcodes movePointRight(2); instead, read the currency via
request.amount().currency() and request.refundAmount().currency(), determine the
correct exponent for that currency (0 for Stripe zero-decimal currencies like
JPY, KRW, etc., otherwise typically 2, or consult a currency-to-decimal map),
then multiply by 10^exponent (e.g. movePointRight(exponent)) and call
longValueExact() to produce the minor-unit string; implement a small helper or
map inside StripePspAdapter to return the exponent for a given currency code and
reuse it from both methods.

Comment on lines +3 to +10
record StripeRefundResponse(
String id,
String status,
Long amount,
String currency,
String paymentIntent
) {
}

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

🌐 Web query:

Does Spring RestClient use Jackson for JSON deserialization by default and does Jackson support snake_case to camelCase mapping automatically?

💡 Result:

  • Yes (when Jackson is on the classpath): Spring’s RestClient uses Spring MVC HTTP message converters, which are registered by default, including a Jackson-based JSON converter that reads/writes JSON using Jackson and requires jackson-databind. (docs.spring.io)

    • (In Spring Framework 6.x this is typically MappingJackson2HttpMessageConverter; in Spring Framework 7.x it’s JacksonJsonHttpMessageConverter.) (docs.spring.io)
  • No, not automatically: Jackson’s default property naming leaves Java property names essentially “as-is” (i.e., it does not auto-map snake_case JSON names to camelCase Java properties unless configured). (fasterxml.github.io)

    • To map snake_casecamelCase, configure a naming strategy such as PropertyNamingStrategies.SnakeCaseStrategy / SNAKE_CASE (or use annotations like @JsonProperty, @JsonNaming). (fasterxml.github.io)

Citations:


🏁 Script executed:

#!/bin/bash

# Search for Jackson configuration classes
find . -type f -name "*.java" | xargs grep -l "PropertyNamingStrategy\|PropertyNamingStrategies\|@JsonNaming" 2>/dev/null | head -20

# Look for Spring configuration files
find . -type f -name "*.java" | xargs grep -l "ObjectMapper\|RestClient\|HttpMessageConverter" 2>/dev/null | grep -i config | head -10

# Search for any Jackson related bean configurations
rg -t java "ObjectMapper|@Configuration|@Bean" -A 3 | grep -A 3 "ObjectMapper" | head -30

Repository: Puneethkumarck/stablebridge-platform

Length of output: 4290


🏁 Script executed:

#!/bin/bash

# Check if there's any global naming strategy configuration in application properties
find . -type f \( -name "application*.properties" -o -name "application*.yml" -o -name "application*.yaml" \) -exec cat {} \; 2>/dev/null | grep -i "jackson\|naming"

# Look for any test files that show how StripeRefundResponse is deserialized
find . -path "*/test*" -name "*.java" | xargs grep -l "StripeRefundResponse" 2>/dev/null

Repository: Puneethkumarck/stablebridge-platform

Length of output: 64


🏁 Script executed:

#!/bin/bash

# Find StripeRefundResponse usage in the codebase
find . -type f -name "*.java" | xargs grep -l "StripeRefundResponse" 2>/dev/null

# Check the actual imports in StripeRefundResponse.java
find . -path "*stripe*" -name "StripeRefundResponse.java" -exec head -20 {} \;

# Look for Stripe client configuration
find . -type f -name "*.java" | xargs grep -l "Stripe\|stripe" 2>/dev/null | grep -i config | head -5

# Search for any Stripe API response mapping examples
find . -type f -name "*.java" | xargs grep -B 3 -A 3 "payment_intent\|paymentIntent" 2>/dev/null | head -40

Repository: Puneethkumarck/stablebridge-platform

Length of output: 7360


🏁 Script executed:

#!/bin/bash

# Check for Jackson dependencies in pom.xml or build.gradle files
find . -name "pom.xml" -o -name "build.gradle" | xargs grep -i "jackson" 2>/dev/null | head -20

# Look for any RestClient usage with StripeRefundResponse
rg -t java "StripeRefundResponse" -B 5 -A 5 | head -50

Repository: Puneethkumarck/stablebridge-platform

Length of output: 3176


🏁 Script executed:

#!/bin/bash

# Check StripePspAdapter.java to see the full refund method and how RestClient is configured
find . -path "*stripe*" -name "StripePspAdapter.java" -exec cat {} \;

# Look for any tests that verify StripeRefundResponse deserialization
find . -path "*/test*" -name "*.java" | xargs grep -l "StripeRefundResponse\|StripePspAdapter" 2>/dev/null | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 5781


🏁 Script executed:

#!/bin/bash

# Check if there are any JSON test fixtures or mocked Stripe responses
find . -name "*.json" | xargs grep -l "payment_intent\|refund" 2>/dev/null | head -10

# Look for RestClient configuration or ObjectMapper bean definitions
rg -t java "@Bean|ObjectMapper|RestClient" -B 2 -A 5 --max-count=50 | head -80

Repository: Puneethkumarck/stablebridge-platform

Length of output: 13392


🏁 Script executed:

#!/bin/bash

# Check the test file for StripeRefundResponse to see if deserialization is tested
cat ./fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java

# Look for WireMock stripe mappings and response files to see actual API response format
find . -path "*wiremock*" -name "*stripe*.json" -exec echo "=== {} ===" \; -exec cat {} \;

Repository: Puneethkumarck/stablebridge-platform

Length of output: 11589


🏁 Script executed:

#!/bin/bash

# Check if there's a parent Spring Boot config or application properties file
find . -name "application*.properties" -o -name "application*.yml" | head -10 | xargs grep -l "jackson\|objectMapper" 2>/dev/null

# Look for any RestClient or Jackson configuration in the fiat-on-ramp module
find ./fiat-on-ramp -name "*.java" | xargs grep -l "ObjectMapper\|@Configuration" 2>/dev/null | head -5

Repository: Puneethkumarck/stablebridge-platform

Length of output: 513


🏁 Script executed:

#!/bin/bash

# Look at the full StripePaymentIntentResponse to see if it also has this issue
find . -path "*stripe*" -name "StripePaymentIntentResponse.java" -exec cat {} \;

# Check if there's any global Jackson configuration in Spring Boot auto-config
find ./fiat-on-ramp -name "*.java" | xargs grep -B 5 -A 5 "objectMapper\|Jackson\|PropertyNamingStrategy" 2>/dev/null | head -40

Repository: Puneethkumarck/stablebridge-platform

Length of output: 309


🏁 Script executed:

#!/bin/bash

# Verify that the test actually expects the paymentIntent field to be deserialized
# by checking if the test asserts on it
grep -A 10 -B 5 "paymentIntent\|payment_intent" ./fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java

# Check the StripePaymentIntentResponse also
find . -type f -name "*.java" -path "*stripe*" | xargs grep -l "StripePaymentIntentResponse" | head -1 | xargs cat

Repository: Puneethkumarck/stablebridge-platform

Length of output: 6717


Add @JsonProperty annotations for snake_case JSON fields in both Stripe response records.

Stripe API returns payment_intent and client_secret (snake_case), but StripeRefundResponse and StripePaymentIntentResponse use camelCase field names. Without explicit mapping, Jackson silently leaves these fields null, creating a silent failure that tests won't catch (they only assert on id and status).

Proposed fix
 package com.stablecoin.payments.onramp.infrastructure.provider.stripe;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 record StripeRefundResponse(
         String id,
         String status,
         Long amount,
         String currency,
-        String paymentIntent
+        `@JsonProperty`("payment_intent") String paymentIntent
 ) {
 }
 package com.stablecoin.payments.onramp.infrastructure.provider.stripe;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 record StripePaymentIntentResponse(
         String id,
         String status,
         Long amount,
         String currency,
-        String clientSecret
+        `@JsonProperty`("client_secret") String clientSecret
 ) {
 }
📝 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
record StripeRefundResponse(
String id,
String status,
Long amount,
String currency,
String paymentIntent
) {
}
record StripeRefundResponse(
String id,
String status,
Long amount,
String currency,
`@JsonProperty`("payment_intent") String paymentIntent
) {
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeRefundResponse.java`
around lines 3 - 10, Stripe API returns snake_case fields but the record fields
are camelCase, so annotate the record components with `@JsonProperty` to map JSON
names to Java names: add import com.fasterxml.jackson.annotation.JsonProperty
and annotate StripeRefundResponse(String id, String status, Long amount, String
currency, `@JsonProperty`("payment_intent") String paymentIntent) and similarly
update StripePaymentIntentResponse to annotate the clientSecret component with
`@JsonProperty`("client_secret"); this ensures Jackson binds payment_intent and
client_secret into paymentIntent and clientSecret respectively.

Comment on lines +120 to +122
assertThatThrownBy(() -> adapter.initiatePayment(aPaymentRequest()))
.isInstanceOf(Exception.class);
}

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

Exception assertion is too broad.

.isInstanceOf(Exception.class) will pass for any exception. Assert on specific exception type (e.g., HttpClientErrorException or WebClientResponseException) to avoid false positives.

As per coding guidelines: "Assert on meaningful values — avoid assertTrue(result != null)"

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

In
`@fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java`
around lines 120 - 122, The current test in StripePspAdapterTest uses a
too-broad assertion (.isInstanceOf(Exception.class)) for
adapter.initiatePayment(aPaymentRequest()); change this to assert a specific
exception type thrown by the HTTP client used in the adapter (for example
replace .isInstanceOf(Exception.class) with
.isInstanceOf(WebClientResponseException.class) or
.isInstanceOf(HttpClientErrorException.class) as appropriate), and optionally
tighten the assertion by checking status/message (e.g.,
.hasMessageContaining(...) or .satisfies(e ->
((WebClientResponseException)e).getStatusCode() == HttpStatus.BAD_REQUEST)) to
ensure meaningful failure checks for initiatePayment.

Comment on lines +145 to +147
assertThatThrownBy(() -> timeoutAdapter.initiatePayment(aPaymentRequest()))
.isInstanceOf(Exception.class);
}

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

Same issue: overly broad exception assertion.

Consider asserting HttpReadTimeoutException or the specific Spring wrapper for timeouts.

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

In
`@fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java`
around lines 145 - 147, The test in StripePspAdapterTest is asserting any
Exception for timeoutAdapter.initiatePayment(aPaymentRequest()), which is too
broad; change the assertion to expect the specific timeout exception (e.g.,
java.net.http.HttpReadTimeoutException or the Spring wrapper such as
org.springframework.web.client.ResourceAccessException caused by a timeout) by
replacing isInstanceOf(Exception.class) with an assertion that targets the
concrete exception type (or the wrapper and its cause) so the test only passes
on real timeout scenarios and references timeoutAdapter.initiatePayment and
aPaymentRequest() when updated.

Comment on lines +220 to +222
assertThatThrownBy(() -> adapter.initiateRefund(aRefundRequest()))
.isInstanceOf(Exception.class);
}

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

Same issue: overly broad exception assertion for 404 case.

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

In
`@fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java`
around lines 220 - 222, The test StripePspAdapterTest currently asserts a
generic Exception for the 404 refund path; update the assertion to expect the
concrete exception type that StripePspAdapter.initiateRefund throws for HTTP 404
(for example NotFoundException, PspResourceNotFoundException, or the specific
StripeApiException used), i.e. replace isInstanceOf(Exception.class) with
isInstanceOf(TheConcrete404Exception.class) and optionally assert the message or
status code to ensure it’s the 404 case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant