Skip to content

Commit e64e159

Browse files
fix(s3): handle charge.succeeded webhook event type (STA-194)
WebhookCommandHandler only handled payment_intent.succeeded but not charge.succeeded. When Stripe sends a charge.succeeded webhook it now routes to the same handlePaymentSucceeded flow and the idempotency check covers both event types. Also corrects the E2E StripeWebhookSender payload to use charge.succeeded. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b8a966a commit e64e159

4 files changed

Lines changed: 68 additions & 3 deletions

File tree

fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandler.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
public class WebhookCommandHandler {
3131

3232
private static final String EVENT_PAYMENT_SUCCEEDED = "payment_intent.succeeded";
33+
private static final String EVENT_CHARGE_SUCCEEDED = "charge.succeeded";
3334
private static final String EVENT_PAYMENT_FAILED = "payment_intent.payment_failed";
3435

3536
private final CollectionOrderRepository orderRepository;
@@ -61,7 +62,7 @@ public CollectionOrder handleWebhook(WebhookCommand command) {
6162
recordPspTransaction(order, command);
6263

6364
return switch (command.eventType()) {
64-
case EVENT_PAYMENT_SUCCEEDED -> handlePaymentSucceeded(order, command);
65+
case EVENT_PAYMENT_SUCCEEDED, EVENT_CHARGE_SUCCEEDED -> handlePaymentSucceeded(order, command);
6566
case EVENT_PAYMENT_FAILED -> handlePaymentFailed(order, command);
6667
default -> {
6768
log.warn("Ignoring unrecognised webhook event type={}", command.eventType());
@@ -138,7 +139,7 @@ private CollectionOrder handlePaymentFailed(CollectionOrder order, WebhookComman
138139

139140
private boolean isAlreadyProcessed(CollectionOrder order, String eventType) {
140141
return switch (eventType) {
141-
case EVENT_PAYMENT_SUCCEEDED ->
142+
case EVENT_PAYMENT_SUCCEEDED, EVENT_CHARGE_SUCCEEDED ->
142143
order.status() == CollectionStatus.COLLECTED
143144
|| order.status() == CollectionStatus.REFUND_INITIATED
144145
|| order.status() == CollectionStatus.REFUND_PROCESSING

fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.anAwaitingConfirmationOrder;
2626
import static com.stablecoin.payments.onramp.fixtures.TestUtils.eqIgnoring;
2727
import static com.stablecoin.payments.onramp.fixtures.TestUtils.eqIgnoringTimestamps;
28+
import static com.stablecoin.payments.onramp.fixtures.WebhookFixtures.aChargeSucceededCommand;
2829
import static com.stablecoin.payments.onramp.fixtures.WebhookFixtures.aFailedCommand;
2930
import static com.stablecoin.payments.onramp.fixtures.WebhookFixtures.aMismatchCommand;
3031
import static com.stablecoin.payments.onramp.fixtures.WebhookFixtures.aSucceededCommand;
@@ -108,6 +109,57 @@ void shouldDetectAmountMismatch() {
108109
}
109110
}
110111

112+
@Nested
113+
@DisplayName("charge.succeeded")
114+
class ChargeSucceeded {
115+
116+
@Test
117+
@DisplayName("should transition AWAITING_CONFIRMATION to COLLECTED and publish CollectionCompletedEvent")
118+
void shouldTransitionToCollectedAndPublishEventForChargeSucceeded() {
119+
// given
120+
var order = anAwaitingConfirmationOrder();
121+
var command = aChargeSucceededCommand();
122+
var collected = order.confirmCollection(command.amount());
123+
124+
given(orderRepository.findByPspReference(PSP_REFERENCE)).willReturn(Optional.of(order));
125+
given(orderRepository.save(eqIgnoringTimestamps(collected))).willReturn(collected);
126+
127+
// when
128+
handler.handleWebhook(command);
129+
130+
// then
131+
then(orderRepository).should().save(eqIgnoringTimestamps(collected));
132+
then(eventPublisher).should().publish(eqIgnoringTimestamps(
133+
new CollectionCompletedEvent(
134+
collected.collectionId(),
135+
collected.paymentId(),
136+
collected.correlationId(),
137+
collected.collectedAmount().amount(),
138+
collected.collectedAmount().currency(),
139+
collected.paymentRail().rail().name(),
140+
collected.psp().pspName(),
141+
collected.pspReference(),
142+
collected.pspSettledAt())));
143+
}
144+
145+
@Test
146+
@DisplayName("should skip duplicate charge.succeeded webhook for already COLLECTED order")
147+
void shouldSkipDuplicateChargeSucceededWebhookForAlreadyCollectedOrder() {
148+
// given
149+
var order = aCollectedOrder();
150+
var command = aChargeSucceededCommand();
151+
152+
given(orderRepository.findByPspReference(PSP_REFERENCE)).willReturn(Optional.of(order));
153+
154+
// when
155+
handler.handleWebhook(command);
156+
157+
// then — no save or event publish should occur
158+
then(orderRepository).should(never()).save(eqIgnoringTimestamps(order));
159+
then(eventPublisher).shouldHaveNoInteractions();
160+
}
161+
}
162+
111163
@Nested
112164
@DisplayName("payment_intent.payment_failed")
113165
class PaymentFailed {

fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ private WebhookFixtures() {}
2222
public static final String WEBHOOK_SECRET = "whsec_test_secret_12345";
2323
public static final String EVENT_ID = "evt_test_001";
2424
public static final String EVENT_TYPE_SUCCEEDED = "payment_intent.succeeded";
25+
public static final String EVENT_TYPE_CHARGE_SUCCEEDED = "charge.succeeded";
2526
public static final String EVENT_TYPE_FAILED = "payment_intent.payment_failed";
2627
public static final UUID COLLECTION_ID = UUID.fromString("c3d4e5f6-a7b8-9012-cdef-123456789012");
2728

@@ -107,6 +108,17 @@ public static WebhookCommand aFailedCommand() {
107108
aFailedEventJson());
108109
}
109110

111+
public static WebhookCommand aChargeSucceededCommand() {
112+
return new WebhookCommand(
113+
EVENT_ID,
114+
EVENT_TYPE_CHARGE_SUCCEEDED,
115+
PSP_REFERENCE,
116+
COLLECTION_ID,
117+
new Money(new BigDecimal("1000.00"), "USD"),
118+
"succeeded",
119+
aSucceededEventJson());
120+
}
121+
110122
public static WebhookCommand aMismatchCommand() {
111123
return new WebhookCommand(
112124
EVENT_ID,

phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ private String buildChargeSucceededPayload(String pspReference, long amount, Str
7474
{
7575
"id": "evt_%s",
7676
"object": "event",
77-
"type": "payment_intent.succeeded",
77+
"type": "charge.succeeded",
7878
"data": {
7979
"object": {
8080
"id": "%s",

0 commit comments

Comments
 (0)