From f35647ddf5a56bc74d45aa6faad45f7de421aa2a Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Mon, 1 Dec 2025 14:15:35 +0000 Subject: [PATCH 1/7] Only sync woocommerce_update_order_item for changed items --- .../sync/src/modules/class-woocommerce.php | 181 +++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-woocommerce.php b/projects/packages/sync/src/modules/class-woocommerce.php index 745f193fdd86f..e02c48f0fedf4 100644 --- a/projects/packages/sync/src/modules/class-woocommerce.php +++ b/projects/packages/sync/src/modules/class-woocommerce.php @@ -64,6 +64,15 @@ class WooCommerce extends Module { */ private $order_item_table_name; + /** + * Per-request map of order_item_id => change info observed during this request. + * + * @access private + * + * @var array + */ + private $order_item_change_map = array(); + /** * The table name. * @@ -128,10 +137,17 @@ public function __construct() { add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 ); add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 ); + // Dedupe order item updates within a request. + add_action( 'woocommerce_before_order_item_object_save', array( $this, 'on_before_order_item_object_save' ), 10, 1 ); + add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_order_item', array( $this, 'on_before_enqueue_update_order_item' ), 1 ); + add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) ); - add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_order_item', array( $this, 'filter_order_item' ) ); add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) ); + // Track meta touches on items within the request. + add_action( 'added_order_item_meta', array( $this, 'on_order_item_meta_added' ), 10, 4 ); + add_action( 'updated_order_item_meta', array( $this, 'on_order_item_meta_updated' ), 10, 4 ); + // Blacklist Action Scheduler comment types. add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 ); @@ -226,6 +242,7 @@ public function get_full_sync_actions() { */ public function init_before_send() { // Full sync. + add_filter( 'jetpack_sync_before_send_woocommerce_update_order_item', array( $this, 'refresh_update_order_item_before_send' ), 5 ); add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'build_full_sync_action_array' ) ); } @@ -238,11 +255,173 @@ public function init_before_send() { * @return array $args The hook arguments. */ public function filter_order_item( $args ) { + if ( ! is_array( $args ) || count( $args ) < 1 ) { + // Upstream filters may return false, in which case pass it through unchanged. + return $args; + } // Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details. $args[1] = $this->build_order_item( $args[0] ); return $args; } + /** + * Refresh the order item data before sending it during a full sync. + * + * @access public + * + * @since $$next-version$$ + * + * @param array $args The hook arguments. + * @return array $args The hook arguments. + */ + public function refresh_update_order_item_before_send( $args ) { + if ( ! is_array( $args ) || count( $args ) < 1 ) { + return $args; + } + $order_item_id = (int) $args[0]; + if ( $order_item_id > 0 ) { + $args[1] = $this->build_order_item( $order_item_id ); + } + return $args; + } + + /** + * Capture changed keys for an order item before it is saved. + * + * @since $$next-version$$ + * + * @param \WC_Order_Item $item The order item object about to be saved. + * @return void + */ + public function on_before_order_item_object_save( $item ) { + if ( ! is_object( $item ) || ! method_exists( $item, 'get_id' ) ) { + return; + } + + $order_item_id = (int) $item->get_id(); + if ( $order_item_id <= 0 ) { + return; + } + + $changes = method_exists( $item, 'get_changes' ) ? (array) $item->get_changes() : array(); + if ( ! isset( $this->order_item_change_map[ $order_item_id ] ) ) { + $this->order_item_change_map[ $order_item_id ] = array( + 'changed_keys' => array(), + 'meta_touched' => false, + 'whitelisted_meta_touched' => false, + 'meta_keys' => array(), + ); + } + // Meta-only: leave changed_keys empty; meta hooks will mark touches. + if ( isset( $changes['meta_data'] ) && count( $changes ) === 1 ) { + $this->order_item_change_map[ $order_item_id ]['changed_keys'] = array(); + return; + } + $this->order_item_change_map[ $order_item_id ]['changed_keys'] = array_keys( $changes ); + } + + /** + * Record meta addition. + * + * @since $$next-version$$ + * + * @param int $meta_id Meta row ID. + * @param int $order_item_id Order item ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @return void + */ + public function on_order_item_meta_added( $meta_id, $order_item_id, $meta_key, $meta_value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Needed for hook signature. + $this->record_item_meta_touch( (int) $order_item_id, (string) $meta_key ); + } + + /** + * Record meta update. + * + * @since $$next-version$$ + * + * @param int $meta_id Meta row ID. + * @param int $order_item_id Order item ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @return void + */ + public function on_order_item_meta_updated( $meta_id, $order_item_id, $meta_key, $meta_value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Needed for hook signature. + $this->record_item_meta_touch( (int) $order_item_id, (string) $meta_key ); + } + + /** + * Mark that an order item had meta touched. + * + * @since $$next-version$$ + * + * @param int $order_item_id Order item ID. + * @param string $meta_key Meta key. + * @return void + */ + private function record_item_meta_touch( $order_item_id, $meta_key ) { + $id = (int) $order_item_id; + $key = trim( (string) $meta_key ); + if ( $id <= 0 || ! $key ) { + return; + } + + if ( ! isset( $this->order_item_change_map[ $id ] ) ) { + $this->order_item_change_map[ $id ] = array( + 'changed_keys' => array(), + 'meta_touched' => false, + 'whitelisted_meta_touched' => false, + 'meta_keys' => array(), + ); + } + + $this->order_item_change_map[ $id ]['meta_touched'] = true; + $this->order_item_change_map[ $id ]['meta_keys'][ $key ] = true; + if ( $this->is_whitelisted_order_item_meta( $key ) ) { + $this->order_item_change_map[ $id ]['whitelisted_meta_touched'] = true; + } + } + /** + * Before enqueueing woocommerce_update_order_item, suppress unchanged items and dedupe per item per request. + * + * @since $$next-version$$ + * + * @param array $args Hook arguments as captured by Sync. + * @return array|false Arguments to enqueue, or false to drop. + */ + public function on_before_enqueue_update_order_item( $args ) { + // Prevent multiple triggers on a single request. + static $processed = array(); + + if ( ! is_array( $args ) || ! isset( $args[0] ) ) { + return $args; + } + + $order_item_id = (int) $args[0]; + if ( $order_item_id <= 0 ) { + return $args; + } + + if ( isset( $processed[ $order_item_id ] ) ) { + return false; + } + + $entry = isset( $this->order_item_change_map[ $order_item_id ] ) ? $this->order_item_change_map[ $order_item_id ] : null; + if ( null === $entry ) { + return false; + } + + $has_non_meta = ! empty( $entry['changed_keys'] ); + $allow = $has_non_meta || ! empty( $entry['meta_touched'] ); + if ( ! $allow ) { + return false; + } + + $processed[ $order_item_id ] = true; + + return array( $order_item_id ); + } + /** * Handler for filtering out non-whitelisted order item meta. * From 7d45975e76304699ad8dacdb7ec23d20128783f6 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Mon, 1 Dec 2025 14:18:46 +0000 Subject: [PATCH 2/7] changelog --- .../sync/changelog/update-sync-less-woo-update-order-items | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/sync/changelog/update-sync-less-woo-update-order-items diff --git a/projects/packages/sync/changelog/update-sync-less-woo-update-order-items b/projects/packages/sync/changelog/update-sync-less-woo-update-order-items new file mode 100644 index 0000000000000..7ed4f5e1c9714 --- /dev/null +++ b/projects/packages/sync/changelog/update-sync-less-woo-update-order-items @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Jetpack Sync: Only sync one woo update order item action per order item update. From 27250e0dcbab61a9856df7fc5464b6e5a7672855 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Tue, 2 Dec 2025 14:54:38 +0000 Subject: [PATCH 3/7] Add a fallback for unknown updates (not caught by get_changes) --- projects/packages/sync/src/modules/class-woocommerce.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-woocommerce.php b/projects/packages/sync/src/modules/class-woocommerce.php index e02c48f0fedf4..3b7c724023925 100644 --- a/projects/packages/sync/src/modules/class-woocommerce.php +++ b/projects/packages/sync/src/modules/class-woocommerce.php @@ -408,7 +408,9 @@ public function on_before_enqueue_update_order_item( $args ) { $entry = isset( $this->order_item_change_map[ $order_item_id ] ) ? $this->order_item_change_map[ $order_item_id ] : null; if ( null === $entry ) { - return false; + // Conservative fallback: allow the first unknown update for this item in this request. + $processed[ $order_item_id ] = true; + return array( $order_item_id ); } $has_non_meta = ! empty( $entry['changed_keys'] ); From 67a62e85c8dbae80351f6d48d1a3d9187e3f0ccd Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Tue, 2 Dec 2025 15:25:12 +0000 Subject: [PATCH 4/7] Ensure the we return have the same arguments as previously, to prevent test / consumer issues --- projects/packages/sync/src/modules/class-woocommerce.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-woocommerce.php b/projects/packages/sync/src/modules/class-woocommerce.php index 3b7c724023925..287d479682c52 100644 --- a/projects/packages/sync/src/modules/class-woocommerce.php +++ b/projects/packages/sync/src/modules/class-woocommerce.php @@ -280,7 +280,14 @@ public function refresh_update_order_item_before_send( $args ) { } $order_item_id = (int) $args[0]; if ( $order_item_id > 0 ) { - $args[1] = $this->build_order_item( $order_item_id ); + $order_item = $this->build_order_item( $order_item_id ); + if ( $order_item ) { + $args[1] = $order_item; + // Ensure the order_id is present as the third argument for consumers/tests expecting it. + if ( ! isset( $args[2] ) && isset( $order_item->order_id ) ) { + $args[2] = (int) $order_item->order_id; + } + } } return $args; } From 0c813c836654f99099058f0dfd2c339630a5c705 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Tue, 2 Dec 2025 15:54:00 +0000 Subject: [PATCH 5/7] Resolve Phan-suggested expression simplification --- projects/packages/sync/src/modules/class-woocommerce.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/sync/src/modules/class-woocommerce.php b/projects/packages/sync/src/modules/class-woocommerce.php index 287d479682c52..441110cc8a89b 100644 --- a/projects/packages/sync/src/modules/class-woocommerce.php +++ b/projects/packages/sync/src/modules/class-woocommerce.php @@ -413,7 +413,7 @@ public function on_before_enqueue_update_order_item( $args ) { return false; } - $entry = isset( $this->order_item_change_map[ $order_item_id ] ) ? $this->order_item_change_map[ $order_item_id ] : null; + $entry = $this->order_item_change_map[ $order_item_id ] ?? null; if ( null === $entry ) { // Conservative fallback: allow the first unknown update for this item in this request. $processed[ $order_item_id ] = true; From 9950492e5e06c6ad6b1ef537a584a7a35d4a8590 Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Tue, 2 Dec 2025 16:08:09 +0000 Subject: [PATCH 6/7] Include a dupe check in tests to prevent any regressions --- .../php/sync/Jetpack_Sync_WooCommerce_Test.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/projects/plugins/jetpack/tests/php/sync/Jetpack_Sync_WooCommerce_Test.php b/projects/plugins/jetpack/tests/php/sync/Jetpack_Sync_WooCommerce_Test.php index c98ff232a9523..164625445012e 100644 --- a/projects/plugins/jetpack/tests/php/sync/Jetpack_Sync_WooCommerce_Test.php +++ b/projects/plugins/jetpack/tests/php/sync/Jetpack_Sync_WooCommerce_Test.php @@ -113,9 +113,20 @@ public function test_updated_order_items_are_synced() { $this->sender->do_sync(); - $update_order_item_event = $this->server_event_storage->get_most_recent_event( 'woocommerce_update_order_item' ); + $all_update_order_item_events = $this->server_event_storage->get_all_events( 'woocommerce_update_order_item' ); + $this->assertNotEmpty( $all_update_order_item_events ); + + $updates_for_order_item = array_values( + array_filter( + $all_update_order_item_events, + function ( $event ) use ( $order_item ) { + return isset( $event->args[0] ) && (int) $event->args[0] === (int) $order_item->get_id(); + } + ) + ); + $this->assertCount( 1, $updates_for_order_item ); - $this->assertTrue( (bool) $update_order_item_event ); + $update_order_item_event = $updates_for_order_item[0]; $this->assertEquals( $order_item->get_id(), $update_order_item_event->args[0] ); $this->assertHasOrderItemProperties( $update_order_item_event->args[1], $order_item ); $this->assertEquals( $order->get_id(), $update_order_item_event->args[2] ); From 69fa90f22fdeb4f3fa6e172a476d99cbebb5e91e Mon Sep 17 00:00:00 2001 From: Karen Attfield Date: Tue, 2 Dec 2025 16:10:19 +0000 Subject: [PATCH 7/7] changelog --- .../jetpack/changelog/update-sync-less-woo-update-order-items | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/jetpack/changelog/update-sync-less-woo-update-order-items diff --git a/projects/plugins/jetpack/changelog/update-sync-less-woo-update-order-items b/projects/plugins/jetpack/changelog/update-sync-less-woo-update-order-items new file mode 100644 index 0000000000000..d4141871f0a7b --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-sync-less-woo-update-order-items @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Tests: Update Sync test to ensure we only sync one WooCommerce order item update action per relevant action.