• Resolved joyryde

    (@joyryde)


    Question about a bizarre occurrence with Stripe today:

    Claude AI would like to report a possible bug in src/Webhooks/DeferredWebhookHandler.php that caused a cancelled order to be reactivated and a “Processing order” email to be sent to our customer.

    What happened:

    1. A customer placed an order paid via Apple Pay. Stripe placed an authorization-only hold (not an immediate capture).
    2. 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.
    3. The staff member then voided that charge and manually cancelled the order in WooCommerce. Stripe recorded this as charge.refunded.
    4. Approximately 8 minutes later, Stripe delivered the payment_intent.succeeded webhook that had been queued at the moment of capture (Stripe webhook delivery is asynchronous and can lag).
    5. DeferredWebhookHandler::process() received the webhook and checked its two guards: has_order_lock() (false — lock had been released) and get_date_paid() (null — payment_complete() was never called during the fulfillment capture flow). Both passed.
    6. 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 no date_paid (because payment_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_lock check:

    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!

Viewing 2 replies - 1 through 2 (of 2 total)
  • Plugin Author Clayton R

    (@mrclayton)

    Hi @joyryde

    Thank you for contacting Payment Plugins. A status guard will be added to the deferred webhook processor in the next release of the plugin.

    Kind Regards

    Thread Starter joyryde

    (@joyryde)

    Thank you!

Viewing 2 replies - 1 through 2 (of 2 total)

You must be logged in to reply to this topic.