{"id":18992876,"date":"2026-08-13T18:35:01","date_gmt":"2026-08-13T18:35:01","guid":{"rendered":"https:\/\/wordpress.org\/support\/topic\/possible-bug-deferredwebhookhandler-re-processes-cancelled-orders-on-del\/"},"modified":"2026-08-13T18:35:01","modified_gmt":"2026-08-13T18:35:01","slug":"possible-bug-deferredwebhookhandler-re-processes-cancelled-orders-on-del","status":"publish","type":"topic","link":"https:\/\/wordpress.org\/support\/topic\/possible-bug-deferredwebhookhandler-re-processes-cancelled-orders-on-del\/","title":{"rendered":"Possible Bug: DeferredWebhookHandler re-processes cancelled orders on del"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Question about a bizarre occurrence with Stripe today:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Claude AI would like to report a possible bug in <code>src\/Webhooks\/DeferredWebhookHandler.php<\/code> that caused a cancelled order to be reactivated and a &#8220;Processing order&#8221; email to be sent to our customer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What happened:<\/strong><\/p>\n\n\n\n<ol>\n<li>A customer placed an order paid via Apple Pay. Stripe placed an authorization-only hold (not an immediate capture).<\/li>\n\n\n\n<li>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 \u2014 the charge was captured.<\/li>\n\n\n\n<li>The staff member then voided that charge and manually cancelled the order in WooCommerce. Stripe recorded this as <code>charge.refunded<\/code>.<\/li>\n\n\n\n<li>Approximately 8 minutes later, Stripe delivered the <code>payment_intent.succeeded<\/code> webhook that had been queued at the moment of capture (Stripe webhook delivery is asynchronous and can lag).<\/li>\n\n\n\n<li><code>DeferredWebhookHandler::process()<\/code> received the webhook and checked its two guards: <code>has_order_lock()<\/code> (false \u2014 lock had been released) and <code>get_date_paid()<\/code> (null \u2014 <code>payment_complete()<\/code> was never called during the fulfillment capture flow). Both passed.<\/li>\n\n\n\n<li>The handler called <code>payment_complete()<\/code> on the already-cancelled order, setting it back to processing, reducing stock, and sending the customer a &#8220;Processing order&#8221; email. The charge was already voided in Stripe \u2014 no money was actually collected.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Root cause:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>DeferredWebhookHandler::process()<\/code> does not check the order&#8217;s current status before re-processing. A cancelled order with no <code>date_paid<\/code> (because <code>payment_complete()<\/code> was not called during the initial capture attempt) is indistinguishable from a legitimate new payment from the handler&#8217;s perspective.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Proposed fix:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Add a status guard after the existing <code>has_order_lock<\/code> check:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">php<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if ( $order-&gt;has_status( &#091; 'cancelled', 'refunded', 'failed' ] ) ) {\n    return;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This prevents the handler from re-activating any order that has already been terminated, regardless of what Stripe delivers afterward.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This scenario (capture attempted but <code>payment_complete()<\/code> not called, then cancellation, then delayed webhook delivery) can realistically recur any time there&#8217;s a gap between capture and WooCommerce&#8217;s payment completion flow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We have created a mu-plugin to work around this bug until it&#8217;s patched.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Thank you for looking into this!<\/p>\n","protected":false},"template":"","class_list":["post-18992876","topic","type-topic","status-publish","hentry"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/18992876","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic"}],"about":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/types\/topic"}],"version-history":[{"count":0,"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/18992876\/revisions"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/media?parent=18992876"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}