ng 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 = $this->get_current_deletion_record_meta_value(); $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", self::DELETED_RECORD_META_KEY, $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(); $order->set_id( $order_id ); $data_store_for_deletion->read( $order ); $data_store_for_deletion->delete( $order, array( 'force_delete' => true, 'suppress_filters' => 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. * * @return int Number of pending records. */ public function get_total_pending_count(): int { return $this->get_current_orders_pending_sync_count(); } /** * Returns the batch with records that needs to be processed for a given size. * * @param int $size Size of the batch. * * @return array Batch of records. */ public function get_next_batch_to_process( int $size ): array { $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; } $updated_order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $order_ids ) ); $order_ids = array_merge( $order_ids, $updated_order_ids ); if ( count( $order_ids ) >= $size ) { return $order_ids; } $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 - count( $order_ids ) ); $order_ids = array_merge( $order_ids, $deleted_order_ids ); return array_map( 'absint', $order_ids ); } /** * Default batch size to use. * * @return int Default batch size. */ public function get_default_batch_size(): int { $batch_size = self::ORDERS_SYNC_BATCH_SIZE; if ( $this->custom_orders_table_is_authoritative() ) { // Back-filling is slower than migration. $batch_size = absint( self::ORDERS_SYNC_BATCH_SIZE / 10 ) + 1; } /** * Filter to customize the count of orders that will be synchronized in each step of the custom orders table to/from posts table synchronization process. * * @since 6.6.0 * * @param int Default value for the count. */ return apply_filters( 'woocommerce_orders_cot_and_posts_sync_step_size', $batch_size ); } /** * A user friendly name for this process. * * @return string Name of the process. */ public function get_name(): string { return 'Order synchronizer'; } /** * A user friendly description for this process. * * @return string Description. */ public function get_description(): string { return 'Synchronizes orders between posts and custom order tables.'; } /** * Prevents deletion of order backup posts (regardless of sync setting) when HPOS is authoritative and the order * still exists in HPOS. * This should help with edge cases where wp_delete_post() would delete the HPOS record too or backfill would sync * incorrect data from an order with no metadata from the posts table. * * @since 8.8.0 * * @param WP_Post|false|null $delete Whether to go forward with deletion. * @param WP_Post $post Post object. * @return WP_Post|false|null */ private function maybe_prevent_deletion_of_post( $delete, $post ) { if ( self::PLACEHOLDER_ORDER_POST_TYPE !== $post->post_type && $this->custom_orders_table_is_authoritative() && $this->data_store->order_exists( $post->ID ) ) { $delete = false; } return $delete; } /** * Handle the 'deleted_post' action. * * When posts is authoritative and sync is enabled, deleting a post also deletes COT data. * * @param int $postid The post id. * @param WP_Post $post The deleted post. */ private function handle_deleted_post( $postid, $post ): void { global $wpdb; $order_post_types = wc_get_order_types( 'cot-migration' ); if ( ! in_array( $post->post_type, $order_post_types, true ) ) { return; } if ( ! $this->get_table_exists() ) { 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) AND NOT EXISTS (SELECT order_id FROM {$this->data_store::get_meta_table_name()} WHERE order_id=%d AND meta_key=%s AND meta_value=%s)", $postid, $postid, self::DELETED_RECORD_META_KEY, self::DELETED_FROM_POSTS_META_VALUE ) ) ) { $wpdb->insert( $this->data_store::get_meta_table_name(), array( 'order_id' => $postid, 'meta_key' => self::DELETED_RECORD_META_KEY, 'meta_value' => self::DELETED_FROM_POSTS_META_VALUE, ) ); } // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.SlowDBQuery } /** * Handle the 'woocommerce_update_order' action. * * When posts is authoritative and sync is enabled, updating a post triggers a corresponding change in the COT table. * * @param int $order_id The order id. */ private function handle_updated_order( $order_id ): void { if ( ! $this->custom_orders_table_is_authoritative() && $this->data_sync_is_enabled() ) { $this->posts_to_cot_migrator->migrate_orders( array( $order_id ) ); } } /** * Handles deletion of auto-draft orders in sync with WP's own auto-draft deletion. * * @since 7.7.0 * * @return void */ private function delete_auto_draft_orders() { if ( ! $this->custom_orders_table_is_authoritative() ) { return; } // Fetch auto-draft orders older than 1 week. $to_delete = wc_get_orders( array( 'date_query' => array( array( 'column' => 'date_created', 'before' => '-1 week', ), ), 'orderby' => 'date', 'order' => 'ASC', 'status' => 'auto-draft', ) ); foreach ( $to_delete as $order ) { $order->delete( true ); } /** * Fires after schedueld deletion of auto-draft orders has been completed. * * @since 7.7.0 */ do_action( 'woocommerce_scheduled_auto_draft_delete' ); } /** * Handles deletion of trashed orders after `EMPTY_TRASH_DAYS` as defined by WordPress. * * @since 8.5.0 * * @return void */ private function delete_trashed_orders() { if ( ! $this->custom_orders_table_is_authoritative() ) { return; } $delete_timestamp = $this->legacy_proxy->call_function( 'time' ) - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS ); $args = array( 'status' => 'trash', 'limit' => self::ORDERS_SYNC_BATCH_SIZE, 'date_modified' => '<' . $delete_timestamp, ); $orders = wc_get_orders( $args ); if ( ! $orders || ! is_array( $orders ) ) { return; } foreach ( $orders as $order ) { if ( $order->get_status() !== 'trash' ) { continue; } if ( $order->get_date_modified()->getTimestamp() >= $delete_timestamp ) { continue; } $order->delete( true ); } } }