HPOS: Implement data synchronization for deleted orders.
This includes the following: - Fix the "upshifting" that was implemented in 36218 so that it only applies when the post type of the order being deleted is hierarchical (this conforms to the WordPress behavior when deleting a post). - Now when an order is deleted while sync is off a record will be created in wp_wc_orders_meta (regardless of which table is the authoritative one) with 'deleted_from' as the key and the authoritative table name (from where the order has been deleted) as the value... - ...then DataSynchronizer will detect the presence of these 'deleted_from' records and delete them from the backup table as part of the batch processing procedure. - Exception to the above: when an order is deleted from the orders table and the corresponding records in the posts table are placeholders, these are deleted immediately too, even if sync is off. - Add an "order_exists" method in both order data stores (Abstract_WC_Order_Data_Store_CPT and OrdersTableDataStore). - Add a pair of get/set_verify_parent_id methods in WC_Abstract_Order (turning off the check in set_parent_id is necessary when deleting an order using the backup data store during synchronization). - Placeholder records are now created with the appropriate parent id for child orders (e.g. refund), this is necessary in order to properly find which records need to be deleted.
This commit is contained in:
parent
55af1d1365
commit
23a605c14c
|
@ -101,6 +101,14 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
||||||
*/
|
*/
|
||||||
protected $object_type = '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.
|
* 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
|
* 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
|
* @since 3.0.0
|
||||||
* @param int $value Value to set.
|
* @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 ) {
|
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->error( 'order_invalid_parent_id', __( 'Invalid parent ID', 'woocommerce' ) );
|
||||||
}
|
}
|
||||||
$this->set_prop( 'parent_id', absint( $value ) );
|
$this->set_prop( 'parent_id', absint( $value ) );
|
||||||
|
@ -2321,4 +2329,27 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
||||||
return __( 'Order', 'woocommerce' );
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
* Method to read an order from the database.
|
||||||
*
|
*
|
||||||
|
|
|
@ -31,9 +31,11 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||||
|
|
||||||
private const ORDERS_SYNC_BATCH_SIZE = 250;
|
private const ORDERS_SYNC_BATCH_SIZE = 250;
|
||||||
// Allowed values for $type in get_ids_of_orders_pending_sync method.
|
// 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_ORDERS_TABLE = 0;
|
||||||
public const ID_TYPE_MISSING_IN_POSTS_TABLE = 1;
|
public const ID_TYPE_MISSING_IN_POSTS_TABLE = 1;
|
||||||
public const ID_TYPE_DIFFERENT_UPDATE_DATE = 2;
|
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.
|
* The data store object to use.
|
||||||
|
@ -69,6 +71,7 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
self::add_action( 'deleted_post', array( $this, 'handle_deleted_post' ), 10, 2 );
|
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_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_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 );
|
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
|
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||||
$pending_count = (int) $wpdb->get_var( $sql );
|
$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 );
|
wp_cache_set( 'woocommerce_hpos_pending_sync_count', $pending_count );
|
||||||
return $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_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_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_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 $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.
|
* @param int $limit Maximum number of ids to return.
|
||||||
|
@ -305,6 +320,10 @@ WHERE
|
||||||
);
|
);
|
||||||
// phpcs:enable
|
// phpcs:enable
|
||||||
break;
|
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:
|
default:
|
||||||
throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' );
|
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" ) );
|
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,
|
* Cleanup all the synchronization status information,
|
||||||
* because the process has been disabled by the user via settings,
|
* because the process has been disabled by the user via settings,
|
||||||
|
@ -329,24 +371,117 @@ WHERE
|
||||||
* @param array $batch Batch details.
|
* @param array $batch Batch details.
|
||||||
*/
|
*/
|
||||||
public function process_batch( array $batch ) : void {
|
public function process_batch( array $batch ) : void {
|
||||||
if ( $this->custom_orders_table_is_authoritative() ) {
|
$custom_orders_table_is_authoritative = $this->custom_orders_table_is_authoritative();
|
||||||
foreach ( $batch as $id ) {
|
$deleted_order_ids = $this->process_deleted_orders( $batch, $custom_orders_table_is_authoritative );
|
||||||
$order = wc_get_order( $id );
|
$batch = array_diff( $batch, $deleted_order_ids );
|
||||||
if ( ! $order ) {
|
|
||||||
$this->error_logger->error( "Order $id not found during batch process, skipping." );
|
if ( ! empty( $batch ) ) {
|
||||||
continue;
|
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();
|
} else {
|
||||||
$data_store->backfill_post_record( $order );
|
$this->posts_to_cot_migrator->migrate_orders( $batch );
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$this->posts_to_cot_migrator->migrate_orders( $batch );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( 0 === $this->get_total_pending_count() ) {
|
if ( 0 === $this->get_total_pending_count() ) {
|
||||||
$this->cleanup_synchronization_state();
|
$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.
|
* Get total number of pending records that require update.
|
||||||
*
|
*
|
||||||
|
@ -364,17 +499,31 @@ WHERE
|
||||||
* @return array Batch of records.
|
* @return array Batch of records.
|
||||||
*/
|
*/
|
||||||
public function get_next_batch_to_process( int $size ): array {
|
public function get_next_batch_to_process( int $size ): array {
|
||||||
if ( $this->custom_orders_table_is_authoritative() ) {
|
$orders_table_is_authoritative = $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(
|
||||||
$order_ids = $this->get_ids_of_orders_pending_sync( self::ID_TYPE_MISSING_IN_ORDERS_TABLE, $size );
|
$orders_table_is_authoritative ? self::ID_TYPE_MISSING_IN_POSTS_TABLE : self::ID_TYPE_MISSING_IN_ORDERS_TABLE,
|
||||||
}
|
$size
|
||||||
|
);
|
||||||
if ( count( $order_ids ) >= $size ) {
|
if ( count( $order_ids ) >= $size ) {
|
||||||
return $order_ids;
|
return $order_ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
$order_ids = $order_ids + $this->get_ids_of_orders_pending_sync( self::ID_TYPE_DIFFERENT_UPDATE_DATE, $size - count( $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.
|
* @param WP_Post $post The deleted post.
|
||||||
*/
|
*/
|
||||||
private function handle_deleted_post( $postid, $post ): void {
|
private function handle_deleted_post( $postid, $post ): void {
|
||||||
if ( 'shop_order' === $post->post_type && $this->data_sync_is_enabled() ) {
|
global $wpdb;
|
||||||
$this->data_store->delete_order_data_from_custom_order_tables( $postid );
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -116,6 +116,20 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||||
*/
|
*/
|
||||||
private $error_logger;
|
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.
|
* 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 ) {
|
final public function init( OrdersTableDataStoreMeta $data_store_meta, DatabaseUtil $database_util, LegacyProxy $legacy_proxy ) {
|
||||||
$this->data_store_meta = $data_store_meta;
|
$this->data_store_meta = $data_store_meta;
|
||||||
$this->database_util = $database_util;
|
$this->database_util = $database_util;
|
||||||
|
$this->legacy_proxy = $legacy_proxy;
|
||||||
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
|
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
|
||||||
$this->internal_meta_keys = $this->get_internal_meta_keys();
|
$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 ] ?? '';
|
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.
|
* Method to read an order from custom tables.
|
||||||
*
|
*
|
||||||
|
@ -1576,6 +1616,7 @@ FROM $order_meta_table
|
||||||
array(
|
array(
|
||||||
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
|
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
|
||||||
'post_status' => 'draft',
|
'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 );
|
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_order_data_from_custom_order_tables( $order_id );
|
||||||
$this->delete_items( $order );
|
$this->delete_items( $order );
|
||||||
|
|
||||||
$order->set_id( 0 );
|
$order->set_id( 0 );
|
||||||
|
|
||||||
// Only delete post data if the posts table is authoritative and synchronization is enabled.
|
// Only delete post data if the orders table is authoritative and synchronization is enabled.
|
||||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
$orders_table_is_authoritative = $order->get_data_store()->get_current_class_name() === self::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}.
|
if ( $orders_table_is_authoritative ) {
|
||||||
// Once we stop creating posts for orders, we should do the cleanup here instead.
|
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||||
wp_delete_post( $order_id );
|
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
|
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.
|
* @param \WC_Abstract_Order $order Order object.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
private function upshift_child_orders( $order ) {
|
private function upshift_or_delete_child_orders( $order ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$order_table = self::get_orders_table_name();
|
|
||||||
$order_parent = $order->get_parent_id();
|
$order_table = self::get_orders_table_name();
|
||||||
$wpdb->update(
|
$order_parent_id = $order->get_parent_id();
|
||||||
$order_table,
|
|
||||||
array( 'parent_order_id' => $order_parent ),
|
if ( $this->legacy_proxy->call_function( 'is_post_type_hierarchical', $order->get_type() ) ) {
|
||||||
array( 'parent_order_id' => $order->get_id() ),
|
$wpdb->update(
|
||||||
array( '%d' ),
|
$order_table,
|
||||||
array( '%d' )
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -66,14 +66,18 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
||||||
$this->delete_order_data_from_custom_order_tables( $refund_id );
|
$this->delete_order_data_from_custom_order_tables( $refund_id );
|
||||||
$refund->set_id( 0 );
|
$refund->set_id( 0 );
|
||||||
|
|
||||||
// If this datastore method is called while the posts table is authoritative, refrain from deleting post data.
|
$orders_table_is_authoritative = $refund->get_data_store()->get_current_class_name() === self::class;
|
||||||
if ( ! is_a( $refund->get_data_store(), self::class ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the associated post, which in turn deletes order items, etc. through {@see WC_Post_Data}.
|
if ( $orders_table_is_authoritative ) {
|
||||||
// Once we stop creating posts for orders, we should do the cleanup here instead.
|
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||||
wp_delete_post( $refund_id );
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -43,6 +43,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
|
||||||
* Initializes system under test.
|
* Initializes system under test.
|
||||||
*/
|
*/
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
|
$this->reset_legacy_proxy_mocks();
|
||||||
$this->original_time_zone = wp_timezone_string();
|
$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.
|
//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' );
|
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.
|
// Remove the Test Suite’s use of temporary tables https://wordpress.stackexchange.com/a/220308.
|
||||||
$this->setup_cot();
|
$this->setup_cot();
|
||||||
$this->toggle_cot( false );
|
$this->toggle_cot( false );
|
||||||
$this->sut = wc_get_container()->get( OrdersTableDataStore::class );
|
$container = wc_get_container();
|
||||||
$this->migrator = wc_get_container()->get( PostsToOrdersMigrationController::class );
|
$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();
|
$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 );
|
$this->toggle_cot( true );
|
||||||
$order = new WC_Order();
|
$order = new WC_Order();
|
||||||
$order->save();
|
$order->save();
|
||||||
|
@ -2022,6 +2033,33 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
|
||||||
$this->assertEquals( 0, $child_order->get_parent_id() );
|
$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.
|
* @testDox Make sure get_order return false when checking an order of different order types without warning.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue