Commit 7b3ecbff6ff for woocommerce
commit 7b3ecbff6ff7c23bc86667f7a03465263894885c
Author: Raluca Stan <ralucastn@gmail.com>
Date: Wed Oct 21 15:41:24 2026 +0200
Fix order.updated webhooks firing for checkout draft orders (#69863)
* Fix order.updated webhooks firing for checkout draft orders
The order webhook delivery check skipped draft, auto-draft and new, but not checkout-draft. The block checkout saves the order as checkout-draft before moving it to pending, so order.updated went out before order.created, and failed checkouts sent order.updated for orders hidden from the admin.
* Add changelog entry for checkout draft webhook fix
diff --git a/plugins/woocommerce/changelog/66995-skip-checkout-draft-order-updated-webhook b/plugins/woocommerce/changelog/66995-skip-checkout-draft-order-updated-webhook
new file mode 100644
index 00000000000..5f7c11dd794
--- /dev/null
+++ b/plugins/woocommerce/changelog/66995-skip-checkout-draft-order-updated-webhook
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop sending "Order updated" webhooks for draft orders created during checkout, so they no longer arrive before "Order created".
diff --git a/plugins/woocommerce/includes/class-wc-webhook.php b/plugins/woocommerce/includes/class-wc-webhook.php
index d30848d5e5c..99045664d95 100644
--- a/plugins/woocommerce/includes/class-wc-webhook.php
+++ b/plugins/woocommerce/includes/class-wc-webhook.php
@@ -332,8 +332,8 @@ class WC_Webhook extends WC_Legacy_Webhook {
$order = wc_get_order( absint( $arg ) );
- // Ignore standard drafts for orders.
- if ( in_array( $order->get_status(), array( OrderStatus::DRAFT, OrderStatus::AUTO_DRAFT, 'new' ), true ) ) {
+ // Ignore drafts for orders, matching the statuses for which the data stores skip woocommerce_new_order.
+ if ( in_array( $order->get_status(), array( OrderStatus::DRAFT, OrderStatus::AUTO_DRAFT, OrderStatus::CHECKOUT_DRAFT, OrderStatus::NEW ), true ) ) {
return false;
}
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php
index 128e8cbee92..18a7ba831fe 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-webhook-tests.php
@@ -3,8 +3,10 @@
* Tests for WC_Webhook class.
*/
+use Automattic\WooCommerce\Enums\OrderStatus;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Tests for WC_Webhook class.
@@ -19,6 +21,27 @@ class WC_Webhook_Test extends WC_Unit_Test_Case {
self::setup_cot_tables();
}
+ /**
+ * Whether HPOS was in use before a test changed the order storage, or null if it did not.
+ *
+ * @var bool|null
+ */
+ private $prev_hpos_enabled = null;
+
+ /**
+ * Restore the order storage a test switched with use_order_storage().
+ */
+ public function tearDown(): void {
+ try {
+ if ( null !== $this->prev_hpos_enabled ) {
+ $this->toggle_cot_authoritative( $this->prev_hpos_enabled );
+ $this->prev_hpos_enabled = null;
+ }
+ } finally {
+ parent::tearDown();
+ }
+ }
+
/**
* @testdox Check that post-action validation uses the affected post ID.
*
@@ -414,6 +437,70 @@ class WC_Webhook_Test extends WC_Unit_Test_Case {
$this->assertSame( array( $coupon_id ), $delivered_ids, 'The variation-only check should not apply to coupon webhooks' );
}
+ /**
+ * @testdox Order updated webhooks are not delivered while the order is a checkout draft.
+ *
+ * @testWith [true]
+ * [false]
+ *
+ * @param bool $hpos_enabled Whether orders are stored in the HPOS tables.
+ */
+ public function test_order_updated_webhook_skips_checkout_draft_orders( bool $hpos_enabled ): void {
+ $this->use_order_storage( $hpos_enabled );
+ $order = new WC_Order();
+ $order->set_status( OrderStatus::CHECKOUT_DRAFT );
+ $order->save();
+ $updated_ids = array();
+ $created_ids = array();
+ $this->record_deliveries( $this->create_active_webhook( 'order.updated' ), $updated_ids );
+ $this->record_deliveries( $this->create_active_webhook( 'order.created' ), $created_ids );
+
+ $order->set_billing_email( 'customer@example.com' );
+ $order->save();
+ $order->update_status( OrderStatus::PENDING );
+
+ $this->assertSame( array(), $updated_ids, 'Saving a checkout draft should not deliver order.updated' );
+ $this->assertSame( array( $order->get_id() ), $created_ids, 'Leaving checkout draft should deliver order.created' );
+ }
+
+ /**
+ * @testdox Order updated webhooks skipped for a checkout draft are still delivered once the order leaves draft.
+ *
+ * @testWith [true]
+ * [false]
+ *
+ * @param bool $hpos_enabled Whether orders are stored in the HPOS tables.
+ */
+ public function test_order_updated_webhook_delivers_after_checkout_draft( bool $hpos_enabled ): void {
+ $this->use_order_storage( $hpos_enabled );
+ $order = new WC_Order();
+ $order->set_status( OrderStatus::CHECKOUT_DRAFT );
+ $order->save();
+ $updated_ids = array();
+ $this->record_deliveries( $this->create_active_webhook( 'order.updated' ), $updated_ids );
+
+ $order->set_billing_email( 'draft@example.com' );
+ $order->save();
+ $order->update_status( OrderStatus::PENDING );
+ $order->set_billing_email( 'customer@example.com' );
+ $order->save();
+
+ $this->assertSame( array( $order->get_id() ), $updated_ids, 'The first save after leaving draft should deliver order.updated once' );
+ }
+
+ /**
+ * Store orders in the HPOS tables or in posts for the current test.
+ *
+ * @param bool $hpos_enabled Whether orders are stored in the HPOS tables.
+ */
+ private function use_order_storage( bool $hpos_enabled ): void {
+ if ( null === $this->prev_hpos_enabled ) {
+ $this->prev_hpos_enabled = OrderUtil::custom_orders_table_usage_is_enabled();
+ }
+ add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+ $this->toggle_cot_authoritative( $hpos_enabled );
+ }
+
/**
* Enqueue a webhook and record the resource IDs it would deliver, without sending them.
*