feat(s3): Stripe PSP adapter + WireMock stubs (STA-124)#127
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
WalkthroughThis 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
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/config/FallbackAdaptersConfig.javafiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePaymentIntentResponse.javafiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeProperties.javafiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapter.javafiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripeRefundResponse.javafiat-on-ramp/fiat-on-ramp/src/main/resources/application.ymlfiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/infrastructure/provider/stripe/StripePspAdapterTest.java
| 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"); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| record StripePaymentIntentResponse( | ||
| String id, | ||
| String status, | ||
| Long amount, | ||
| String currency, | ||
| String clientSecret | ||
| ) { | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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".
| 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()); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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()); |
There was a problem hiding this comment.
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.
| 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()); | ||
| } |
There was a problem hiding this comment.
🧩 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 -150Repository: 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 -5Repository: Puneethkumarck/stablebridge-platform
Length of output: 62
🏁 Script executed:
rg "PspRefundRequest" --type java -A 10 | head -50Repository: Puneethkumarck/stablebridge-platform
Length of output: 7001
🏁 Script executed:
rg "class Money|record Money" --type java -A 5 | head -30Repository: 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.
| record StripeRefundResponse( | ||
| String id, | ||
| String status, | ||
| Long amount, | ||
| String currency, | ||
| String paymentIntent | ||
| ) { | ||
| } |
There was a problem hiding this comment.
🧩 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
RestClientuses Spring MVC HTTP message converters, which are registered by default, including a Jackson-based JSON converter that reads/writes JSON using Jackson and requiresjackson-databind. (docs.spring.io)- (In Spring Framework 6.x this is typically
MappingJackson2HttpMessageConverter; in Spring Framework 7.x it’sJacksonJsonHttpMessageConverter.) (docs.spring.io)
- (In Spring Framework 6.x this is typically
-
No, not automatically: Jackson’s default property naming leaves Java property names essentially “as-is” (i.e., it does not auto-map
snake_caseJSON names tocamelCaseJava properties unless configured). (fasterxml.github.io)- To map
snake_case↔camelCase, configure a naming strategy such asPropertyNamingStrategies.SnakeCaseStrategy/SNAKE_CASE(or use annotations like@JsonProperty,@JsonNaming). (fasterxml.github.io)
- To map
Citations:
- 1: https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
- 2: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.html?utm_source=openai
- 3: https://fasterxml.github.io/jackson-databind/javadoc/2.14/com/fasterxml/jackson/databind/PropertyNamingStrategy.html?utm_source=openai
- 4: https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/PropertyNamingStrategies.SnakeCaseStrategy.html?utm_source=openai
🏁 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 -30Repository: 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/nullRepository: 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 -40Repository: 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 -50Repository: 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 -5Repository: 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 -80Repository: 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 -5Repository: 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 -40Repository: 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 catRepository: 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.
| 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.
| assertThatThrownBy(() -> adapter.initiatePayment(aPaymentRequest())) | ||
| .isInstanceOf(Exception.class); | ||
| } |
There was a problem hiding this comment.
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.
| assertThatThrownBy(() -> timeoutAdapter.initiatePayment(aPaymentRequest())) | ||
| .isInstanceOf(Exception.class); | ||
| } |
There was a problem hiding this comment.
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.
| assertThatThrownBy(() -> adapter.initiateRefund(aRefundRequest())) | ||
| .isInstanceOf(Exception.class); | ||
| } |
There was a problem hiding this comment.
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.
Summary
StripePspAdapteras ACL adapter forPspGatewaydomain port — form-encoded requests to Stripe API (PaymentIntents + Refunds)StripePropertieswith@ConfigurationProperties(prefix = "app.psp.stripe")— configurable base URL, API key, timeoutStripePaymentIntentResponse,StripeRefundResponse) withLongwrapper types (Jackson 3 compat)@CircuitBreaker(name = "stripe")on both methods with hard-block fallbacks (re-throw)RestClientwithHttpClient.Version.HTTP_1_1(avoids WireMock EOF errors)PspGatewaybean inFallbackAdaptersConfigvia@ConditionalOnMissingBeanTest plan
Closes STA-124
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Configuration
Tests