diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index 78932ad68bc..0859130a1ef 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -101,6 +101,14 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { */ protected $object_type = 'order'; + /** + * Indicates if set_parent_id will throw an error if no order exists with the supplied id. + * + * @since 7.7.0 + * @var bool + */ + private $verify_parent_id = true; + /** * Get the order if ID is passed, otherwise the order is new and empty. * This class should NOT be instantiated, but the wc_get_order function or new WC_Order_Factory @@ -569,10 +577,10 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { * * @since 3.0.0 * @param int $value Value to set. - * @throws WC_Data_Exception Exception thrown if parent ID does not exist or is invalid. + * @throws WC_Data_Exception Exception thrown if parent ID does not exist or is invalid (only if $verify_parent_id is true). */ public function set_parent_id( $value ) { - if ( $value && ( $value === $this->get_id() || ! wc_get_order( $value ) ) ) { + if ( $this->verify_parent_id && $value && ( $value === $this->get_id() || ! wc_get_order( $value ) ) ) { $this->error( 'order_invalid_parent_id', __( 'Invalid parent ID', 'woocommerce' ) ); } $this->set_prop( 'parent_id', absint( $value ) ); @@ -2321,4 +2329,27 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { return __( 'Order', 'woocommerce' ); } } + + /** + * Sets the value of the $verify_parent_id flag. + * + * If the flag is set, set_parent_id will throw an error if no order exists with the supplied id. + * + * @param bool $value + * @return void + */ + public function set_verify_parent_id( bool $value ) { + $this->verify_parent_id = $value; + } + + /** + * Gets the value of the $verify_parent_id flag. + * + * If the flag is set, set_parent_id will throw an error if no order exists with the supplied id. + * + * @return bool + */ + public function get_verify_parent_id(): bool { + return $this->verify_parent_id; + } } diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php index fad3a54eca4..194732fc8a2 100644 --- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -106,6 +106,23 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme } } + /** + * Check if an order exists by id. + * + * @since 7.7.0 + * + * @param int $order_id The order id to check. + * @return bool True if an order exists with the given name. + */ + public function order_exists( $order_id ) : bool { + if ( ! $order_id ) { + return false; + } + + $post_object = get_post( $order_id ); + return ! is_null( $post_object ) && in_array( $post_object->post_type, wc_get_order_types(), true ); + } + /** * Method to read an order from the database. * diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index efb50a8940c..19c59f16f26 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -31,9 +31,11 @@ class DataSynchronizer implements BatchProcessorInterface { private const ORDERS_SYNC_BATCH_SIZE = 250; // Allowed values for $type in get_ids_of_orders_pending_sync method. - public const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0; - public const ID_TYPE_MISSING_IN_POSTS_TABLE = 1; - public const ID_TYPE_DIFFERENT_UPDATE_DATE = 2; + public const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0; + public const ID_TYPE_MISSING_IN_POSTS_TABLE = 1; + public const ID_TYPE_DIFFERENT_UPDATE_DATE = 2; + public const ID_TYPE_DELETED_FROM_ORDERS_TABLE = 3; + public const ID_TYPE_DELETED_FROM_POSTS_TABLE = 4; /** * The data store object to use. @@ -69,6 +71,7 @@ class DataSynchronizer implements BatchProcessorInterface { public function __construct() { self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 ); self::add_action( 'woocommerce_new_order', array( $this, 'handle_updated_order' ), 100 ); + self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 ); self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 ); self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 ); } @@ -228,6 +231,16 @@ SELECT( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $pending_count = (int) $wpdb->get_var( $sql ); + + $deleted_from_table = $this->custom_orders_table_is_authoritative() ? $orders_table : $wpdb->posts; + $deleted_count = $wpdb->get_var( + $wpdb->prepare( + "SELECT count(1) FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key=%s AND meta_value=%s", + array( 'deleted_from', $deleted_from_table ) + ) + ); + $pending_count += $deleted_count; + wp_cache_set( 'woocommerce_hpos_pending_sync_count', $pending_count ); return $pending_count; } @@ -249,6 +262,8 @@ SELECT( * ID_TYPE_MISSING_IN_ORDERS_TABLE: orders that exist in posts table but not in orders table. * ID_TYPE_MISSING_IN_POSTS_TABLE: orders that exist in orders table but not in posts table (the corresponding post entries are placeholders). * ID_TYPE_DIFFERENT_UPDATE_DATE: orders that exist in both tables but have different last update dates. + * ID_TYPE_DELETED_FROM_ORDERS_TABLE: orders deleted from the orders table but not yet from the posts table. + * ID_TYPE_DELETED_FROM_POSTS_TABLE: orders deleted from the posts table but not yet from the orders table. * * @param int $type One of ID_TYPE_MISSING_IN_ORDERS_TABLE, ID_TYPE_MISSING_IN_POSTS_TABLE, ID_TYPE_DIFFERENT_UPDATE_DATE. * @param int $limit Maximum number of ids to return. @@ -305,6 +320,10 @@ WHERE ); // phpcs:enable break; + case self::ID_TYPE_DELETED_FROM_ORDERS_TABLE: + return $this->get_deleted_order_ids( true, $limit ); + case self::ID_TYPE_DELETED_FROM_POSTS_TABLE: + return $this->get_deleted_order_ids( false, $limit ); default: throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' ); } @@ -314,6 +333,29 @@ WHERE return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) ); } + /** + * Get the ids of the orders that are marked as deleted in the orders meta table. + * + * @param bool $deleted_from_orders_table True to get the ids of the orders deleted from the orders table, false o get the ids of the orders deleted from the posts table. + * @param int $limit The maximum count of orders to return. + * @return array An array of order ids. + */ + private function get_deleted_order_ids( bool $deleted_from_orders_table, int $limit ) { + global $wpdb; + + $deleted_from_table = $deleted_from_orders_table ? $this->data_store::get_orders_table_name() : $wpdb->posts; + $order_ids = $wpdb->get_col( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->prepare( + "SELECT DISTINCT(order_id) FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key=%s AND meta_value=%s LIMIT {$limit}", + array( 'deleted_from', $deleted_from_table ) + ) + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + + return $order_ids; + } + /** * Cleanup all the synchronization status information, * because the process has been disabled by the user via settings, @@ -329,24 +371,117 @@ WHERE * @param array $batch Batch details. */ public function process_batch( array $batch ) : void { - if ( $this->custom_orders_table_is_authoritative() ) { - foreach ( $batch as $id ) { - $order = wc_get_order( $id ); - if ( ! $order ) { - $this->error_logger->error( "Order $id not found during batch process, skipping." ); - continue; + $custom_orders_table_is_authoritative = $this->custom_orders_table_is_authoritative(); + $deleted_order_ids = $this->process_deleted_orders( $batch, $custom_orders_table_is_authoritative ); + $batch = array_diff( $batch, $deleted_order_ids ); + + if ( ! empty( $batch ) ) { + if ( $custom_orders_table_is_authoritative ) { + foreach ( $batch as $id ) { + $order = wc_get_order( $id ); + if ( ! $order ) { + $this->error_logger->error( "Order $id not found during batch process, skipping." ); + continue; + } + $data_store = $order->get_data_store(); + $data_store->backfill_post_record( $order ); } - $data_store = $order->get_data_store(); - $data_store->backfill_post_record( $order ); + } else { + $this->posts_to_cot_migrator->migrate_orders( $batch ); } - } else { - $this->posts_to_cot_migrator->migrate_orders( $batch ); } + if ( 0 === $this->get_total_pending_count() ) { $this->cleanup_synchronization_state(); } } + /** + * Take a batch of order ids pending synchronization and process those that were deleted, ignoring the others + * (which will be orders that were created or modified) and returning the ids of the orders actually processed. + * + * @param array $batch Array of ids of order pending synchronization. + * @param bool $custom_orders_table_is_authoritative True if the custom orders table is currently authoritative. + * @return array Order ids that have been actually processed. + */ + private function process_deleted_orders( array $batch, bool $custom_orders_table_is_authoritative ): array { + global $wpdb; + + $deleted_from_table_name = $custom_orders_table_is_authoritative ? $this->data_store::get_orders_table_name() : $wpdb->posts; + $data_store_for_deletion = + $custom_orders_table_is_authoritative ? + new \WC_Order_Data_Store_CPT() : + wc_get_container()->get( OrdersTableDataStore::class ); + + $order_ids_as_sql_list = '(' . implode( ',', $batch ) . ')'; + + $deleted_order_ids = array(); + $meta_ids_to_delete = array(); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $deletion_data = $wpdb->get_results( + $wpdb->prepare( + "SELECT id, order_id FROM {$wpdb->prefix}wc_orders_meta WHERE meta_key=%s AND meta_value=%s AND order_id IN $order_ids_as_sql_list ORDER BY order_id DESC", + 'deleted_from', + $deleted_from_table_name + ), + ARRAY_A + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + if ( empty( $deletion_data ) ) { + return array(); + } + + foreach ( $deletion_data as $item ) { + $meta_id = $item['id']; + $order_id = $item['order_id']; + + if ( isset( $deleted_order_ids[ $order_id ] ) ) { + $meta_ids_to_delete[] = $meta_id; + continue; + } + + if ( ! $data_store_for_deletion->order_exists( $order_id ) ) { + $this->error_logger->warning( "Order {$order_id} doesn't exist in the backup table, thus it can't be deleted" ); + $deleted_order_ids[] = $order_id; + $meta_ids_to_delete[] = $meta_id; + continue; + } + + try { + $order = new \WC_Order(); + + //We need to turn parent order verification off to avoid an exception for child orders (e.g. refunds). + //The 'read' method in the order class invokes 'set_parent_id', which by default verifies + //if the order the parent id refers to actually exists, and throws an exception if not. + //To check if the order exists 'wc_get_order' is used, but this function searches the order + //using the authoritative data store, and at this point the order doesn't exist anymore in the + //authoritative table ('read' below is being invoked in the backup data store). + $order->set_verify_parent_id( false ); + + $order->set_id( $order_id ); + $data_store_for_deletion->read( $order ); + + $data_store_for_deletion->delete( $order, array( 'force_delete' => true ) ); + } catch ( \Exception $ex ) { + $this->error_logger->error( "Couldn't delete order {$order_id} from the backup table: {$ex->getMessage()}" ); + continue; + } + + $deleted_order_ids[] = $order_id; + $meta_ids_to_delete[] = $meta_id; + } + + if ( ! empty( $meta_ids_to_delete ) ) { + $order_id_rows_as_sql_list = '(' . implode( ',', $meta_ids_to_delete ) . ')'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_orders_meta WHERE id IN {$order_id_rows_as_sql_list}" ); + } + + return $deleted_order_ids; + } + /** * Get total number of pending records that require update. * @@ -364,17 +499,31 @@ WHERE * @return array Batch of records. */ public function get_next_batch_to_process( int $size ): array { - if ( $this->custom_orders_table_is_authoritative() ) { - $order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_POSTS_TABLE, $size ); - } else { - $order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_ORDERS_TABLE, $size ); - } + $orders_table_is_authoritative = $this->custom_orders_table_is_authoritative(); + + $order_ids = $this->get_ids_of_orders_pending_sync( + $orders_table_is_authoritative ? self::ID_TYPE_MISSING_IN_POSTS_TABLE : self::ID_TYPE_MISSING_IN_ORDERS_TABLE, + $size + ); if ( count( $order_ids ) >= $size ) { return $order_ids; } $order_ids = $order_ids + $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $order_ids ) ); - return $order_ids; + if ( count( $order_ids ) > 0 ) { + return $order_ids; + } + + // Deleted orders pending sync have the lowest priority, and additionally, + // if such orders exist then we return just those, even if they don't fill a batch. + // Returning "missing orders that are to be deleted" and "missing orders that are to be created" + // in the same batch is confusing and potentially dangerous. + $deleted_order_ids = $this->get_ids_of_orders_pending_sync( + $orders_table_is_authoritative ? self::ID_TYPE_DELETED_FROM_ORDERS_TABLE : self::ID_TYPE_DELETED_FROM_POSTS_TABLE, + $size + ); + + return $deleted_order_ids; } /** @@ -426,9 +575,42 @@ WHERE * @param WP_Post $post The deleted post. */ private function handle_deleted_post( $postid, $post ): void { - if ( 'shop_order' === $post->post_type && $this->data_sync_is_enabled() ) { - $this->data_store->delete_order_data_from_custom_order_tables( $postid ); + global $wpdb; + + if ( 'shop_order' !== $post->post_type && 'shop_order_refund' !== $post->post_type ) { + return; } + + $features_controller = wc_get_container()->get( FeaturesController::class ); + $feature_is_enabled = $features_controller->feature_is_enabled( 'custom_order_tables' ); + if ( ! $feature_is_enabled ) { + return; + } + + if ( $this->data_sync_is_enabled() ) { + $this->data_store->delete_order_data_from_custom_order_tables( $postid ); + } elseif ( $this->custom_orders_table_is_authoritative() ) { + return; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery + if ( $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS (SELECT id FROM {$this->data_store::get_orders_table_name()} WHERE ID=%d)", + $postid + ) + ) + ) { + $wpdb->insert( + $this->data_store::get_meta_table_name(), + array( + 'order_id' => $postid, + 'meta_key' => 'deleted_from', + 'meta_value' => $wpdb->posts, + ) + ); + } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery } /** diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index a78e7976eee..6d4ccc1e820 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -116,6 +116,20 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements */ private $error_logger; + /** + * The name of the main orders table. + * + * @var string + */ + private $orders_table_name; + + /** + * The instance of the LegacyProxy object to use. + * + * @var LegacyProxy + */ + private $legacy_proxy; + /** * Initialize the object. * @@ -129,8 +143,11 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements final public function init( OrdersTableDataStoreMeta $data_store_meta, DatabaseUtil $database_util, LegacyProxy $legacy_proxy ) { $this->data_store_meta = $data_store_meta; $this->database_util = $database_util; + $this->legacy_proxy = $legacy_proxy; $this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' ); $this->internal_meta_keys = $this->get_internal_meta_keys(); + + $this->orders_table_name = self::get_orders_table_name(); } /** @@ -955,6 +972,29 @@ WHERE return $type[ $order_id ] ?? ''; } + /** + * Check if an order exists by id. + * + * @since 7.7.0 + * + * @param int $order_id The order id to check. + * @return bool True if an order exists with the given name. + */ + public function order_exists( $order_id ) : bool { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $exists = $wpdb->get_var( + $wpdb->prepare( + "SELECT EXISTS (SELECT id FROM {$this->orders_table_name} WHERE id=%d)", + $order_id + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + return (bool) $exists; + } + /** * Method to read an order from custom tables. * @@ -1576,6 +1616,7 @@ FROM $order_meta_table array( 'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE, 'post_status' => 'draft', + 'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0, ) ); @@ -1761,18 +1802,24 @@ FROM $order_meta_table */ do_action( 'woocommerce_before_delete_order', $order_id, $order ); - $this->upshift_child_orders( $order ); + $this->upshift_or_delete_child_orders( $order ); $this->delete_order_data_from_custom_order_tables( $order_id ); $this->delete_items( $order ); $order->set_id( 0 ); - // Only delete post data if the posts table is authoritative and synchronization is enabled. - $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); - if ( $data_synchronizer->data_sync_is_enabled() && $order->get_data_store()->get_current_class_name() === self::class ) { - // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. - // Once we stop creating posts for orders, we should do the cleanup here instead. - wp_delete_post( $order_id ); + // Only delete post data if the orders table is authoritative and synchronization is enabled. + $orders_table_is_authoritative = $order->get_data_store()->get_current_class_name() === self::class; + + if ( $orders_table_is_authoritative ) { + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() ) { + // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. + // Once we stop creating posts for orders, we should do the cleanup here instead. + wp_delete_post( $order_id ); + } else { + $this->handle_order_deletion_with_sync_disabled( $order_id ); + } } do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment @@ -1794,23 +1841,89 @@ FROM $order_meta_table } /** - * Helper method to set child orders to the parent order's parent. + * Handles the deletion of an order from the orders table when sync is disabled: + * + * If the corresponding row in the posts table is of placeholder type, + * it's just deleted; otherwise a "deleted_from" record is created in the meta table + * and the sync process will detect these and take care of deleting the appropriate post records. + * + * @param int $order_id Th id of the order that has been deleted from the orders table. + * @return void + */ + protected function handle_order_deletion_with_sync_disabled( $order_id ): void { + global $wpdb; + + $post_type = $wpdb->get_var( + $wpdb->prepare( "SELECT post_type FROM {$wpdb->posts} WHERE ID=%d", $order_id ) + ); + + if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE === $post_type ) { + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->posts} WHERE ID=%d OR post_parent=%d", + $order_id, + $order_id + ) + ); + } else { + $related_order_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT id FROM {$wpdb->posts} WHERE post_parent = %d", + $order_id + ) + ); + $related_order_ids[] = $order_id; + + // phpcs:disable WordPress.DB.SlowDBQuery + foreach ( $related_order_ids as $id ) { + $wpdb->insert( + self::get_meta_table_name(), + array( + 'order_id' => $id, + 'meta_key' => 'deleted_from', + 'meta_value' => self::get_orders_table_name(), + ) + ); + } + // phpcs:enable WordPress.DB.SlowDBQuery + } + } + + /** + * Set the parent id of child orders to the parent order's parent if the post type + * for the order is hierarchical, just deletes the child orders otherwise. * * @param \WC_Abstract_Order $order Order object. * * @return void */ - private function upshift_child_orders( $order ) { + private function upshift_or_delete_child_orders( $order ) { global $wpdb; - $order_table = self::get_orders_table_name(); - $order_parent = $order->get_parent_id(); - $wpdb->update( - $order_table, - array( 'parent_order_id' => $order_parent ), - array( 'parent_order_id' => $order->get_id() ), - array( '%d' ), - array( '%d' ) - ); + + $order_table = self::get_orders_table_name(); + $order_parent_id = $order->get_parent_id(); + + if ( $this->legacy_proxy->call_function( 'is_post_type_hierarchical', $order->get_type() ) ) { + $wpdb->update( + $order_table, + array( 'parent_order_id' => $order_parent_id ), + array( 'parent_order_id' => $order->get_id() ), + array( '%d' ), + array( '%d' ) + ); + } else { + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $child_order_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT id FROM $order_table WHERE parent_order_id=%d", + $order->get_id() + ) + ); + foreach ( $child_order_ids as $child_order_id ) { + $this->delete_order_data_from_custom_order_tables( $child_order_id ); + } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } } /** diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php index 43fa4acaf28..2c4583559c6 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php @@ -66,14 +66,18 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore { $this->delete_order_data_from_custom_order_tables( $refund_id ); $refund->set_id( 0 ); - // If this datastore method is called while the posts table is authoritative, refrain from deleting post data. - if ( ! is_a( $refund->get_data_store(), self::class ) ) { - return; - } + $orders_table_is_authoritative = $refund->get_data_store()->get_current_class_name() === self::class; - // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. - // Once we stop creating posts for orders, we should do the cleanup here instead. - wp_delete_post( $refund_id ); + if ( $orders_table_is_authoritative ) { + $data_synchronizer = wc_get_container()->get( DataSynchronizer::class ); + if ( $data_synchronizer->data_sync_is_enabled() ) { + // Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}. + // Once we stop creating posts for orders, we should do the cleanup here instead. + wp_delete_post( $refund_id ); + } else { + $this->handle_order_deletion_with_sync_disabled( $refund_id ); + } + } } /** diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 47538063371..3d30cd7734b 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -43,6 +43,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { * Initializes system under test. */ public function setUp(): void { + $this->reset_legacy_proxy_mocks(); $this->original_time_zone = wp_timezone_string(); //phpcs:ignore WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set -- We need to change the timezone to test the date sync fields. update_option( 'timezone_string', 'Asia/Kolkata' ); @@ -50,8 +51,10 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { // Remove the Test Suiteā€™s use of temporary tables https://wordpress.stackexchange.com/a/220308. $this->setup_cot(); $this->toggle_cot( false ); - $this->sut = wc_get_container()->get( OrdersTableDataStore::class ); - $this->migrator = wc_get_container()->get( PostsToOrdersMigrationController::class ); + $container = wc_get_container(); + $container->reset_all_resolved(); + $this->sut = $container->get( OrdersTableDataStore::class ); + $this->migrator = $container->get( PostsToOrdersMigrationController::class ); $this->cpt_data_store = new WC_Order_Data_Store_CPT(); } @@ -2004,9 +2007,17 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { } /** - * @testDox When parent order is deleted, child orders should be upshifted. + * @testDox When parent order is deleted, and the post order type is hierarchical, child orders should be upshifted. */ - public function test_child_orders_are_promoted_when_parent_is_deleted() { + public function test_child_orders_are_promoted_when_parent_is_deleted_if_order_type_is_hierarchical() { + $this->register_legacy_proxy_function_mocks( + array( + 'is_post_type_hierarchical' => function( $post_type ) { + return 'shop_order' === $post_type || is_post_type_hierarchical( $post_type ); + }, + ) + ); + $this->toggle_cot( true ); $order = new WC_Order(); $order->save(); @@ -2022,6 +2033,33 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->assertEquals( 0, $child_order->get_parent_id() ); } + /** + * @testDox When parent order is deleted, and the post order type is NOT hierarchical, child orders should be deleted. + */ + public function test_child_orders_are_promoted_when_parent_is_deleted_if_order_type_is_not_hierarchical() { + $this->register_legacy_proxy_function_mocks( + array( + 'is_post_type_hierarchical' => function( $post_type ) { + return 'shop_order' === $post_type ? false : is_post_type_hierarchical( $post_type ); + }, + ) + ); + + $this->toggle_cot( true ); + $order = new WC_Order(); + $order->save(); + + $child_order = new WC_Order(); + $child_order->set_parent_id( $order->get_id() ); + $child_order->save(); + + $this->assertEquals( $order->get_id(), $child_order->get_parent_id() ); + $this->sut->delete( $order, array( 'force_delete' => true ) ); + $child_order = wc_get_order( $child_order->get_id() ); + + $this->assertFalse( $child_order ); + } + /** * @testDox Make sure get_order return false when checking an order of different order types without warning. */