Skip to content

fix(s3): handle charge.succeeded webhook event type (STA-194)#184

Merged
Puneethkumarck merged 2 commits into
mainfrom
feature/STA-194-handle-charge-succeeded-webhook
Mar 16, 2026
Merged

fix(s3): handle charge.succeeded webhook event type (STA-194)#184
Puneethkumarck merged 2 commits into
mainfrom
feature/STA-194-handle-charge-succeeded-webhook

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add charge.succeeded event handling to WebhookCommandHandler (previously only handled payment_intent.succeeded)
  • Fix StripeWebhookSender E2E support class to send correct event type
  • Add 2 unit tests for the new event type (happy path + idempotency)

Closes STA-194

Test plan

  • ./gradlew :fiat-on-ramp:fiat-on-ramp:test — all tests pass
  • ./gradlew :fiat-on-ramp:fiat-on-ramp:spotlessCheck — clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • The payment system now accepts and processes charge.succeeded webhook events like existing payment-success events.
    • Webhook handling includes stronger idempotency checks to prevent duplicate processing when orders are already collected or in refund states.
  • Tests

    • Added test coverage for charge.succeeded handling and duplicate-webhook scenarios.

@coderabbitai

coderabbitai Bot commented Mar 16, 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 6 minutes and 26 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: 7d60f71a-9c90-496f-a159-8dd38c8a2d59

📥 Commits

Reviewing files that changed from the base of the PR and between ad75ba7 and 4f41b75.

📒 Files selected for processing (2)
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java
  • fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java

Walkthrough

Adds handling for a new charge.succeeded webhook event: a new event constant, routing that event to the existing payment-succeeded handler (including idempotency gating), test fixtures and unit tests, and an integration test helper emitting charge.succeeded.

Changes

Cohort / File(s) Summary
Webhook Handler Service
fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandler.java
Introduced EVENT_CHARGE_SUCCEEDED and dispatches charge.succeeded to the same handlePaymentSucceeded() path; extended idempotency checks to include charge.succeeded alongside the payment-succeeded event.
Unit Tests & Fixtures
fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java, fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java
Added aChargeSucceededCommand() fixture and EVENT_TYPE_CHARGE_SUCCEEDED; added ChargeSucceeded nested test class with tests for successful transition to COLLECTED and duplicate-webhook skipping.
Integration Test Support
phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java
Updated test payload builder to emit event type charge.succeeded (replacing payment_intent.succeeded).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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 main change: adding charge.succeeded webhook event handling.
Description check ✅ Passed Description covers summary, related issue, changes, and test validation. Type of Change and Breaking Changes sections are not checked, but core required sections are present and specific.

✏️ 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-194-handle-charge-succeeded-webhook
📝 Coding Plan
  • Generate coding plan for human review comments

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

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>
@Puneethkumarck Puneethkumarck force-pushed the feature/STA-194-handle-charge-succeeded-webhook branch from e64e159 to ad75ba7 Compare March 16, 2026 05:46

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java (1)

72-88: ⚠️ Potential issue | 🟠 Major

Fix webhook payload schema to match Stripe's charge.succeeded event structure.

Line 81 incorrectly emits "object": "payment_intent" for a charge.succeeded event. Per Stripe API docs, data.object must be a Charge resource with "object": "charge", and the PaymentIntent reference should reside at data.object.payment_intent. This mismatch produces a non-representative test payload that can mask webhook parser defects.

Proposed fix
 private String buildChargeSucceededPayload(String pspReference, long amount, String currency) {
     return """
             {
                 "id": "evt_%s",
                 "object": "event",
                 "type": "charge.succeeded",
                 "data": {
                     "object": {
                         "id": "%s",
-                        "object": "payment_intent",
+                        "object": "charge",
+                        "payment_intent": "%s",
                         "amount": %d,
                         "currency": "%s",
                         "status": "succeeded"
                     }
                 }
             }
-            """.formatted(pspReference, pspReference, amount, currency);
+            """.formatted(pspReference, pspReference, pspReference, amount, currency);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java`
around lines 72 - 88, The payload built by buildChargeSucceededPayload is using
the wrong resource type; change the JSON under data.object to represent a Stripe
Charge (set "object": "charge") and add a "payment_intent" property pointing to
a PaymentIntent id (either pass a paymentIntentId into
buildChargeSucceededPayload or derive one, e.g. "pi_"+pspReference), then update
the formatted placeholders to include that payment intent id so the emitted
webhook matches Stripe's charge.succeeded schema.
🤖 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/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java`:
- Around line 145-160: The test should also assert that no persistence of PSP
transactions occurs for duplicate charge.succeeded webhooks: in the test method
shouldSkipDuplicateChargeSucceededWebhookForAlreadyCollectedOrder() add an
assertion that pspTransactionRepository had no interactions (or did not save any
transaction) after calling handler.handleWebhook(command), similar to the
existing then(orderRepository).should(never()).save(...) and
then(eventPublisher).shouldHaveNoInteractions(); reference
pspTransactionRepository to locate the mock and use
then(pspTransactionRepository).shouldHaveNoInteractions() or
then(pspTransactionRepository).should(never()).save(...) as appropriate.

In
`@fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java`:
- Around line 111-120: The test helper aChargeSucceededCommand() is constructing
a WebhookCommand with EVENT_TYPE_CHARGE_SUCCEEDED but reuses
aSucceededEventJson(), which emits a payment_intent.succeeded payload—replace
the raw JSON payload with one that matches the charge.succeeded shape (create or
call aChargeSucceededEventJson() or similar) so the payload type aligns with the
EVENT_TYPE_CHARGE_SUCCEEDED used by aChargeSucceededCommand() and downstream
parsers/auditors like WebhookCommand consumers.

---

Outside diff comments:
In
`@phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java`:
- Around line 72-88: The payload built by buildChargeSucceededPayload is using
the wrong resource type; change the JSON under data.object to represent a Stripe
Charge (set "object": "charge") and add a "payment_intent" property pointing to
a PaymentIntent id (either pass a paymentIntentId into
buildChargeSucceededPayload or derive one, e.g. "pi_"+pspReference), then update
the formatted placeholders to include that payment intent id so the emitted
webhook matches Stripe's charge.succeeded schema.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 78119700-8884-4325-becb-0c24db0e30c4

📥 Commits

Reviewing files that changed from the base of the PR and between 0bb1429 and e64e159.

📒 Files selected for processing (4)
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandler.java
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java
  • fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java
  • phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java

…pTransaction assertion (STA-194)

- Add dedicated aChargeSucceededEventJson() with correct type field
- Add pspTransactionRepository.shouldHaveNoInteractions() to duplicate test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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.

♻️ Duplicate comments (2)
fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java (1)

111-120: ⚠️ Potential issue | 🟠 Major

aChargeSucceededCommand() uses a mismatched raw payload type.

Line 119 still uses aSucceededEventJson() (payment_intent.succeeded) while the command type is charge.succeeded. Keep event type and raw payload consistent.

Proposed fix
 public static WebhookCommand aChargeSucceededCommand() {
     return new WebhookCommand(
             EVENT_ID,
             EVENT_TYPE_CHARGE_SUCCEEDED,
             PSP_REFERENCE,
             COLLECTION_ID,
             new Money(new BigDecimal("1000.00"), "USD"),
             "succeeded",
-            aSucceededEventJson());
+            aChargeSucceededEventJson());
 }
+
+public static String aChargeSucceededEventJson() {
+    return """
+            {
+              "id": "%s",
+              "type": "charge.succeeded",
+              "data": {
+                "object": {
+                  "id": "%s",
+                  "amount": 100000,
+                  "currency": "usd",
+                  "status": "succeeded",
+                  "metadata": {
+                    "collection_id": "%s"
+                  }
+                }
+              }
+            }
+            """.formatted(EVENT_ID, PSP_REFERENCE, COLLECTION_ID);
+}
🤖 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/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java`
around lines 111 - 120, The helper aChargeSucceededCommand() constructs a
charge.succeeded WebhookCommand but uses the mismatched raw payload
aSucceededEventJson() (payment_intent.succeeded); update the raw payload to the
correct charge payload by calling the appropriate fixture (e.g.,
aChargeSucceededEventJson()) or create a new aChargeSucceededEventJson() method
and use that in aChargeSucceededCommand() so EVENT_TYPE_CHARGE_SUCCEEDED and the
raw payload are consistent.
fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java (1)

145-160: ⚠️ Potential issue | 🟠 Major

Strengthen duplicate-charge idempotency assertion for PSP transaction persistence.

Please also assert no interaction with pspTransactionRepository; currently this test only covers order save/event publish side effects.

Proposed test hardening
         // then — no save or event publish should occur
         then(orderRepository).should(never()).save(eqIgnoringTimestamps(order));
         then(eventPublisher).shouldHaveNoInteractions();
+        then(pspTransactionRepository).shouldHaveNoInteractions();

As per coding guidelines "Collection status tracking must handle webhook idempotency".

🤖 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/domain/service/WebhookCommandHandlerTest.java`
around lines 145 - 160, The test should assert that no PSP transaction
persistence occurs for duplicate charge webhooks; add an assertion in
shouldSkipDuplicateChargeSucceededWebhookForAlreadyCollectedOrder to verify
pspTransactionRepository has no interactions (e.g.,
then(pspTransactionRepository).shouldHaveNoInteractions()) after calling
handler.handleWebhook(command) so the test covers both order and PSP-transaction
idempotency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java`:
- Around line 145-160: The test should assert that no PSP transaction
persistence occurs for duplicate charge webhooks; add an assertion in
shouldSkipDuplicateChargeSucceededWebhookForAlreadyCollectedOrder to verify
pspTransactionRepository has no interactions (e.g.,
then(pspTransactionRepository).shouldHaveNoInteractions()) after calling
handler.handleWebhook(command) so the test covers both order and PSP-transaction
idempotency.

In
`@fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java`:
- Around line 111-120: The helper aChargeSucceededCommand() constructs a
charge.succeeded WebhookCommand but uses the mismatched raw payload
aSucceededEventJson() (payment_intent.succeeded); update the raw payload to the
correct charge payload by calling the appropriate fixture (e.g.,
aChargeSucceededEventJson()) or create a new aChargeSucceededEventJson() method
and use that in aChargeSucceededCommand() so EVENT_TYPE_CHARGE_SUCCEEDED and the
raw payload are consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bd186b1d-9f86-4757-bec0-37a47512c042

📥 Commits

Reviewing files that changed from the base of the PR and between e64e159 and ad75ba7.

📒 Files selected for processing (4)
  • fiat-on-ramp/fiat-on-ramp/src/main/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandler.java
  • fiat-on-ramp/fiat-on-ramp/src/test/java/com/stablecoin/payments/onramp/domain/service/WebhookCommandHandlerTest.java
  • fiat-on-ramp/fiat-on-ramp/src/testFixtures/java/com/stablecoin/payments/onramp/fixtures/WebhookFixtures.java
  • phase3-integration-tests/src/test/java/com/stablecoin/payments/phase3/support/StripeWebhookSender.java

@Puneethkumarck Puneethkumarck merged commit 9c259c1 into main Mar 16, 2026
12 of 13 checks passed
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