Possible Bug: DeferredWebhookHandler re-processes cancelled orders on del
-
Question about a bizarre occurrence with Stripe today:
Claude AI would like to report a possible bug in
src/Webhooks/DeferredWebhookHandler.phpthat caused a cancelled order to be reactivated and a “Processing order” email to be sent to our customer.What happened:
- A customer placed an order paid via Apple Pay. Stripe placed an authorization-only hold (not an immediate capture).
- Six weeks later, a staff member attempted to capture the charge through our fulfillment workflow. The original Stripe authorization had expired, so the first capture attempt failed. A second attempt succeeded — the charge was captured.
- The staff member then voided that charge and manually cancelled the order in WooCommerce. Stripe recorded this as
charge.refunded. - Approximately 8 minutes later, Stripe delivered the
payment_intent.succeededwebhook that had been queued at the moment of capture (Stripe webhook delivery is asynchronous and can lag). DeferredWebhookHandler::process()received the webhook and checked its two guards:has_order_lock()(false — lock had been released) andget_date_paid()(null —payment_complete()was never called during the fulfillment capture flow). Both passed.- The handler called
payment_complete()on the already-cancelled order, setting it back to processing, reducing stock, and sending the customer a “Processing order” email. The charge was already voided in Stripe — no money was actually collected.
Root cause:
DeferredWebhookHandler::process()does not check the order’s current status before re-processing. A cancelled order with nodate_paid(becausepayment_complete()was not called during the initial capture attempt) is indistinguishable from a legitimate new payment from the handler’s perspective.Proposed fix:
Add a status guard after the existing
has_order_lockcheck:php
if ( $order->has_status( [ 'cancelled', 'refunded', 'failed' ] ) ) { return; }This prevents the handler from re-activating any order that has already been terminated, regardless of what Stripe delivers afterward.
This scenario (capture attempted but
payment_complete()not called, then cancellation, then delayed webhook delivery) can realistically recur any time there’s a gap between capture and WooCommerce’s payment completion flow.We have created a mu-plugin to work around this bug until it’s patched.
Thank you for looking into this!
You must be logged in to reply to this topic.