Skip to content

Lock webhook handlers against concurrent duplicate deliveries - #321

Merged
sandervanhooft merged 3 commits into
mainfrom
sandervanhooft/webhook-concurrency
Apr 23, 2026
Merged

Lock webhook handlers against concurrent duplicate deliveries#321
sandervanhooft merged 3 commits into
mainfrom
sandervanhooft/webhook-concurrency

Conversation

@sandervanhooft

@sandervanhooft sandervanhooft commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Protects customers and merchants from the financial side effects of duplicate webhook deliveries. Without this change, a retried or concurrent webhook can cause a customer to be credited twice for the same failed payment, receive two refund orders for a single refund, or have a chargeback counted more than once. This PR makes the paid, failed, refund, and chargeback webhook flows safe to deliver more than once, with no schema changes, migrations, cache locks, or new drivers required.

Changes

  • Order::handlePaymentPaid / handlePaymentFailed: reload the order with lockForUpdate, bail if already paid/failed, and only dispatch OrderPaymentPaid / OrderPaymentFailed when this call actually transitioned the row. Payment lookup switches to findByPaymentIdOrFail now that first-payment idempotency guarantees the row exists (Fix idempotent first payment webhooks #320).
  • Refund::handleProcessed / handleFailed: lock the refund, bail unless still pending, and gate the RefundProcessed / RefundFailed event on the winning transaction so the compensating order and item hooks run once.
  • AftercareWebhookController chargeback branch: wrap the charged-back comparison and update in a transaction that locks the payment row before recomputing the delta, so concurrent deliveries cannot both dispatch ChargebackReceived for the same amount.

Tests

  • Adds stale-instance regression coverage in OrderTest and RefundTest. Each handler is invoked on an outdated model instance whose row has already transitioned, and the test asserts no duplicate event, no duplicate credit/refund order, and no second increment.
  • Adds ChargebackStaleReadPayment fixture and an aftercare test proving a stale amount_charged_back read does not produce a duplicate ChargebackReceived.
  • Cleans up an unrelated PHPUnit deprecation notice in AftercareWebhookControllerTest.

@sandervanhooft sandervanhooft changed the title Harden duplicate webhook handling Harden payment and refund concurrency handling Apr 23, 2026
@sandervanhooft sandervanhooft changed the title Harden payment and refund concurrency handling Lock webhook handlers against concurrent duplicate deliveries Apr 23, 2026
Replace `! lessThan` with `greaterThanOrEqual` for readability in the
aftercare chargeback branch, and add a stale-read regression test that
proves concurrent deliveries dispatch `ChargebackReceived` only once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sandervanhooft
sandervanhooft merged commit 529da22 into main Apr 23, 2026
34 checks passed
@sandervanhooft
sandervanhooft deleted the sandervanhooft/webhook-concurrency branch April 23, 2026 21:32
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