[HPOS] Synchronize order deletions (#37050)

This commit is contained in:
Vedanshu Jain 2023-07-11 17:16:18 +05:30 committed by GitHub
commit 7fbb12b274
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1259 additions and 126 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add synchronization of deleted orders for HPOS

View File

@ -232,6 +232,9 @@ class WC_Install {
'7.7.0' => array(
'wc_update_770_remove_multichannel_marketing_feature_options',
),
'8.0.0' => array(
'wc_update_800_delete_stray_order_records',
),
);
/**

View File

@ -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 8.0.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.
*
@ -242,7 +259,8 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
$args = wp_parse_args(
$args,
array(
'force_delete' => false,
'force_delete' => false,
'suppress_filters' => false,
)
);
@ -250,14 +268,60 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
return;
}
$do_filters = ! $args['suppress_filters'];
if ( $args['force_delete'] ) {
if ( $do_filters ) {
/**
* Fires immediately before an order is deleted from the database.
*
* @since 8.0.0
*
* @param int $order_id ID of the order about to be deleted.
* @param WC_Order $order Instance of the order that is about to be deleted.
*/
do_action( 'woocommerce_before_delete_order', $id, $order );
}
wp_delete_post( $id );
$order->set_id( 0 );
do_action( 'woocommerce_delete_order', $id );
if ( $do_filters ) {
/**
* Fires immediately after an order is deleted.
*
* @since
*
* @param int $order_id ID of the order that has been deleted.
*/
do_action( 'woocommerce_delete_order', $id );
}
} else {
if ( $do_filters ) {
/**
* Fires immediately before an order is trashed.
*
* @since 8.0.0
*
* @param int $order_id ID of the order about to be trashed.
* @param WC_Order $order Instance of the order that is about to be trashed.
*/
do_action( 'woocommerce_before_trash_order', $id, $order );
}
wp_trash_post( $id );
$order->set_status( 'trash' );
do_action( 'woocommerce_trash_order', $id );
if ( $do_filters ) {
/**
* Fires immediately after an order is trashed.
*
* @since
*
* @param int $order_id ID of the order that has been trashed.
*/
do_action( 'woocommerce_trash_order', $id );
}
}
}

View File

@ -879,7 +879,7 @@ function wc_update_total_sales_counts( $order_id ) {
$recorded_sales = $order->get_data_store()->get_recorded_sales( $order );
$reflected_order = in_array( $order->get_status(), array( 'cancelled', 'trash' ), true );
if ( ! $reflected_order && 'before_delete_post' === current_action() ) {
if ( ! $reflected_order && 'woocommerce_before_delete_order' === current_action() ) {
$reflected_order = true;
}
@ -919,11 +919,9 @@ add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' )
add_action( 'woocommerce_order_status_completed_to_cancelled', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_processing_to_cancelled', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_order_status_on-hold_to_cancelled', 'wc_update_total_sales_counts' );
add_action( 'trashed_post', 'wc_update_total_sales_counts' );
add_action( 'untrashed_post', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_trash_order', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_untrash_order', 'wc_update_total_sales_counts' );
add_action( 'before_delete_post', 'wc_update_total_sales_counts' );
add_action( 'woocommerce_before_delete_order', 'wc_update_total_sales_counts' );
/**
* Update used coupon amount for each coupon within an order.

View File

@ -21,10 +21,12 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs;
use Automattic\WooCommerce\Internal\AssignDefaultCategory;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize as Download_Directories_Sync;
use Automattic\WooCommerce\Utilities\StringUtil;
/**
* Update file paths for 2.0
@ -2589,3 +2591,55 @@ function wc_update_770_remove_multichannel_marketing_feature_options() {
delete_option( 'woocommerce_multichannel_marketing_enabled' );
delete_option( 'woocommerce_marketing_overview_welcome_hidden' );
}
/**
* Delete posts of type "shop_order_placeholder" with no matching order in the orders table.
*/
function wc_update_800_delete_stray_order_records() {
global $wpdb;
$orders_table_name = OrdersTableDataStore::get_orders_table_name();
// phpcs:disable WordPress.DB.PreparedSQL
$old_max_id = get_option( 'woocommerce_update_800_delete_stray_order_records_last_processed_id', 0 );
if ( 0 === $old_max_id ) {
$suppress = $wpdb->suppress_errors();
$orders_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $orders_table_name ) ) );
$wpdb->suppress_errors( $suppress );
if ( ! $orders_table_exists ) {
return false;
};
}
$new_max_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT MAX(id) FROM (SELECT id FROM $orders_table_name WHERE id > %d ORDER BY id LIMIT 10000) x",
$old_max_id
)
);
if ( null === $new_max_id ) {
delete_option( 'woocommerce_update_800_delete_stray_order_records_last_processed_id' );
return false;
}
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->posts} WHERE post_type = %s AND ID > %d AND ID <= %d AND ID NOT IN (SELECT id FROM $orders_table_name WHERE id > %d AND id <= %d)",
'shop_order_placehold',
$old_max_id,
$new_max_id,
$old_max_id,
$new_max_id,
)
);
// phpcs:enable WordPress.DB.PreparedSQL
update_option( 'woocommerce_update_800_delete_stray_order_records_last_processed_id', $new_max_id );
return true;
}

View File

@ -31,11 +31,18 @@ class DataSynchronizer implements BatchProcessorInterface {
public const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished';
public const PLACEHOLDER_ORDER_POST_TYPE = 'shop_order_placehold';
public const DELETED_RECORD_META_KEY = '_deleted_from';
public const DELETED_FROM_POSTS_META_VALUE = 'posts_table';
public const DELETED_FROM_ORDERS_META_VALUE = 'orders_table';
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.
@ -78,6 +85,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_action( 'wp_scheduled_auto_draft_delete', array( $this, 'delete_auto_draft_orders' ), 9 );
@ -247,10 +255,32 @@ SELECT(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$pending_count = (int) $wpdb->get_var( $sql );
$deleted_from_table = $this->get_current_deletion_record_meta_value();
$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( self::DELETED_RECORD_META_KEY, $deleted_from_table )
)
);
$pending_count += $deleted_count;
wp_cache_set( 'woocommerce_hpos_pending_sync_count', $pending_count );
return $pending_count;
}
/**
* Get the meta value for order deletion records based on which table is currently authoritative.
*
* @return string self::DELETED_FROM_ORDERS_META_VALUE if the orders table is authoritative, self::DELETED_FROM_POSTS_META_VALUE otherwise.
*/
private function get_current_deletion_record_meta_value() {
return $this->custom_orders_table_is_authoritative() ?
self::DELETED_FROM_ORDERS_META_VALUE :
self::DELETED_FROM_POSTS_META_VALUE;
}
/**
* Is the custom orders table the authoritative data source for orders currently?
*
@ -268,6 +298,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.
@ -327,6 +359,10 @@ ORDER BY orders.id ASC
);
// 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.' );
}
@ -336,6 +372,31 @@ ORDER BY orders.id ASC
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 = $this->get_current_deletion_record_meta_value();
$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}",
self::DELETED_RECORD_META_KEY,
$deleted_from_table
)
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
return array_map( 'absint', $order_ids );
}
/**
* Cleanup all the synchronization status information,
* because the process has been disabled by the user via settings,
@ -351,27 +412,124 @@ ORDER BY orders.id ASC
* @param array $batch Batch details.
*/
public function process_batch( array $batch ) : void {
if ( empty( $batch ) ) {
return;
}
$batch = array_map( 'absint', $batch );
$this->order_cache_controller->temporarily_disable_orders_cache_usage();
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();
$this->order_cache_controller->maybe_restore_orders_cache_usage();
}
}
/**
* 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 = $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.
*
@ -389,17 +547,29 @@ ORDER BY orders.id ASC
* @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;
$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 );
}
/**
@ -451,9 +621,47 @@ ORDER BY orders.id ASC
* @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;
$order_post_types = wc_get_order_types( 'cot-migration' );
if ( ! in_array( $post->post_type, $order_post_types, true ) ) {
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)
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
}
/**

View File

@ -117,6 +117,13 @@ 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.
*
@ -140,6 +147,8 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
$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();
}
/**
@ -986,6 +995,29 @@ WHERE
return $type[ $order_id ] ?? '';
}
/**
* Check if an order exists by id.
*
* @since 8.0.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.
*
@ -1626,6 +1658,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,
)
);
@ -1877,17 +1910,29 @@ FROM $order_meta_table
return;
}
if ( ! empty( $args['force_delete'] ) ) {
$args = wp_parse_args(
$args,
array(
'force_delete' => false,
'suppress_filters' => false,
)
);
/**
* Fires immediately before an order is deleted from the database.
*
* @since 7.1.0
*
* @param int $order_id ID of the order about to be deleted.
* @param WC_Order $order Instance of the order that is about to be deleted.
*/
do_action( 'woocommerce_before_delete_order', $order_id, $order );
$do_filters = ! $args['suppress_filters'];
if ( $args['force_delete'] ) {
if ( $do_filters ) {
/**
* Fires immediately before an order is deleted from the database.
*
* @since 7.1.0
*
* @param int $order_id ID of the order about to be deleted.
* @param WC_Order $order Instance of the order that is about to be deleted.
*/
do_action( 'woocommerce_before_delete_order', $order_id, $order );
}
$this->upshift_or_delete_child_orders( $order );
$this->delete_order_data_from_custom_order_tables( $order_id );
@ -1901,28 +1946,98 @@ FROM $order_meta_table
*
* In other words, we do not delete the post record when HPOS table is authoritative and synchronization is disabled but post record is a full record and not just a placeholder, because it implies that the order was created before HPOS was enabled.
*/
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
if ( $data_synchronizer->data_sync_is_enabled() || get_post_type( $order_id ) === 'shop_order_placehold' ) {
// 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 );
$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
if ( $do_filters ) {
/**
* Fires immediately after an order is deleted.
*
* @since
*
* @param int $order_id ID of the order that has been deleted.
*/
do_action( 'woocommerce_delete_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
} else {
/**
* Fires immediately before an order is trashed.
*
* @since 7.1.0
*
* @param int $order_id ID of the order about to be deleted.
* @param WC_Order $order Instance of the order that is about to be deleted.
*/
do_action( 'woocommerce_before_trash_order', $order_id, $order );
if ( $do_filters ) {
/**
* Fires immediately before an order is trashed.
*
* @since 7.1.0
*
* @param int $order_id ID of the order about to be trashed.
* @param WC_Order $order Instance of the order that is about to be trashed.
*/
do_action( 'woocommerce_before_trash_order', $order_id, $order );
}
$this->trash_order( $order );
do_action( 'woocommerce_trash_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
if ( $do_filters ) {
/**
* Fires immediately after an order is trashed.
*
* @since
*
* @param int $order_id ID of the order that has been trashed.
*/
do_action( 'woocommerce_trash_order', $order_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
}
}
/**
* 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 {
// phpcs:disable WordPress.DB.SlowDBQuery
$wpdb->insert(
self::get_meta_table_name(),
array(
'order_id' => $order_id,
'meta_key' => DataSynchronizer::DELETED_RECORD_META_KEY,
'meta_value' => DataSynchronizer::DELETED_FROM_ORDERS_META_VALUE,
)
);
// phpcs:enable WordPress.DB.SlowDBQuery
// Note that at this point upshift_or_delete_child_orders will already have been invoked,
// thus all the child orders either still exist but have a different parent id,
// or have been deleted and got their own deletion record already.
// So there's no need to do anything about them.
}
}
@ -1934,7 +2049,7 @@ FROM $order_meta_table
*
* @return void
*/
private function upshift_or_delete_child_orders( $order ) {
private function upshift_or_delete_child_orders( $order ) : void {
global $wpdb;
$order_table = self::get_orders_table_name();

View File

@ -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 );
}
}
}
/**

View File

@ -104,4 +104,33 @@ final class StringUtil {
public static function is_null_or_whitespace( ?string $value ) {
return is_null( $value ) || '' === $value || ctype_space( $value );
}
/**
* Convert an array of values to a list suitable for a SQL "IN" statement
* (so comma separated and delimited by parenthesis).
* e.g.: [1,2,3] --> (1,2,3)
*
* @param array $values The values to convert.
* @return string A parenthesized and comma-separated string generated from the values.
* @throws \InvalidArgumentException Empty values array passed.
*/
public static function to_sql_list( array $values ) {
if ( empty( $values ) ) {
throw new \InvalidArgumentException( self::class_name_without_namespace( __CLASS__ ) . '::' . __FUNCTION__ . ': the values array is empty' );
}
return '(' . implode( ',', $values ) . ')';
}
/**
* Get the name of a class without the namespace.
*
* @param string $class_name The full class name.
* @return string The class name without the namespace.
*/
public static function class_name_without_namespace( string $class_name ) {
// A '?:' would convert this to a one-liner, but WP coding standards disallow these :shrug:.
$result = substr( strrchr( $class_name, '\\' ), 1 );
return $result ? $result : $class_name;
}
}

View File

@ -154,7 +154,7 @@ class WC_Unit_Tests_Bootstrap {
private function initialize_hpos() {
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::delete_order_custom_tables();
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order_custom_table_if_not_exist();
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::toggle_cot( true );
\Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::toggle_cot_feature_and_usage( true );
}
/**

View File

@ -158,7 +158,7 @@ class OrderHelper {
* @param boolean $enabled TRUE to enable COT or FALSE to disable.
* @return void
*/
public static function toggle_cot( bool $enabled ) {
public static function toggle_cot_feature_and_usage( bool $enabled ) {
$features_controller = wc_get_container()->get( Featurescontroller::class );
$features_controller->change_feature_enable( 'custom_order_tables', $enabled );
@ -189,7 +189,7 @@ class OrderHelper {
*/
public static function create_complex_wp_post_order() {
$current_cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
self::toggle_cot( false );
self::toggle_cot_feature_and_usage( false );
update_option( 'woocommerce_prices_include_tax', 'yes' );
update_option( 'woocommerce_calc_taxes', 'yes' );
$uniq_cust_id = wp_generate_password( 10, false );
@ -259,7 +259,7 @@ class OrderHelper {
$order->save();
$order->save_meta_data();
self::toggle_cot( $current_cot_state );
self::toggle_cot_feature_and_usage( $current_cot_state );
return $order->get_id();
}
@ -353,5 +353,4 @@ class OrderHelper {
return $order;
}
}

View File

@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\RestApi\UnitTests;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
@ -24,14 +25,14 @@ trait HPOSToggleTrait {
OrderHelper::delete_order_custom_tables();
OrderHelper::create_order_custom_table_if_not_exist();
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
}
/**
* Call in teardown to disable COT/HPOS.
*/
public function clean_up_cot_setup(): void {
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
// Add back removed filter.
add_filter( 'query', array( $this, '_create_temporary_tables' ) );
@ -39,13 +40,21 @@ trait HPOSToggleTrait {
}
/**
* Enables or disables the custom orders table across WP temporarily.
* Enables or disables the custom orders table feature, and sets the orders table as authoritative, across WP temporarily.
*
* @param boolean $enabled TRUE to enable COT or FALSE to disable.
* @return void
*/
private function toggle_cot( bool $enabled ): void {
OrderHelper::toggle_cot( $enabled );
private function toggle_cot_feature_and_usage( bool $enabled ): void {
OrderHelper::toggle_cot_feature_and_usage( $enabled );
}
/**
* Set the orders table or the posts table as the authoritative table to store orders.
* @param bool $cot_authoritative True to set the orders table as authoritative, false to set the posts table as authoritative.
*/
protected function toggle_cot_authoritative( bool $cot_authoritative ) {
update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, wc_bool_to_string( $cot_authoritative ) );
}
/**

View File

@ -23,7 +23,7 @@ class WC_Order_Factory_Test extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
$this->cot_state = \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
OrderHelper::toggle_cot( false );
OrderHelper::toggle_cot_feature_and_usage( false );
}
/**
@ -34,7 +34,7 @@ class WC_Order_Factory_Test extends WC_Unit_Test_Case {
public function tearDown(): void {
parent::tearDown();
wp_cache_flush();
OrderHelper::toggle_cot( $this->cot_state );
OrderHelper::toggle_cot_feature_and_usage( $this->cot_state );
}
/**
@ -81,7 +81,7 @@ class WC_Order_Factory_Test extends WC_Unit_Test_Case {
* @testDox Test that cache does not interfere with order sorting.
*/
public function test_cache_dont_interfere_with_orders() {
OrderHelper::toggle_cot( $this->cot_state );
OrderHelper::toggle_cot_feature_and_usage( $this->cot_state );
$order1 = OrderHelper::create_order();
$order2 = OrderHelper::create_order();
@ -93,7 +93,7 @@ class WC_Order_Factory_Test extends WC_Unit_Test_Case {
$this->assertEquals( 2, count( $orders ) );
$this->assertEquals( $order1->get_id(), $orders[0]->get_id() );
$this->assertEquals( $order2->get_id(), $orders[1]->get_id() );
OrderHelper::toggle_cot( false );
OrderHelper::toggle_cot_feature_and_usage( false );
}
}

View File

@ -23,7 +23,7 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
$this->prev_cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
OrderHelper::toggle_cot( false );
OrderHelper::toggle_cot_feature_and_usage( false );
}
/**
@ -32,7 +32,7 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
* @return void
*/
public function tearDown(): void {
OrderHelper::toggle_cot( $this->prev_cot_state );
OrderHelper::toggle_cot_feature_and_usage( $this->prev_cot_state );
parent::tearDown();
}
@ -293,4 +293,64 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
$this->assertTrue( $order->untrash(), 'The order was restored from the trash.' );
$this->assertEquals( $original_status, $order->get_status(), 'The original order status is restored following untrash.' );
}
/**
* @testDox A 'suppress_filters' argument can be passed to 'delete', if true no 'woocommerce_(before_)trash/delete_order' actions will be fired.
*
* @testWith [null, true]
* [true, true]
* [false, true]
* [null, false]
* [true, false]
* [false, false]
*
* @param bool|null $suppress True or false to use a 'suppress_filters' argument with that value, null to not use it.
* @param bool $force_delete True to delete the order, false to trash it.
* @return void
*/
public function test_filters_can_be_suppressed_when_trashing_or_deleting_an_order( ?bool $suppress, bool $force_delete ) {
$order_id_from_before_delete = null;
$order_id_from_after_delete = null;
$order_from_before_delete = null;
$trash_or_delete = $force_delete ? 'delete' : 'trash';
add_action(
"woocommerce_before_{$trash_or_delete}_order",
function ( $order_id, $order ) use ( &$order_id_from_before_delete, &$order_from_before_delete ) {
$order_id_from_before_delete = $order_id;
$order_from_before_delete = $order;
},
10,
2
);
add_action(
"woocommerce_{$trash_or_delete}_order",
function ( $order_id ) use ( &$order_id_from_after_delete ) {
$order_id_from_after_delete = $order_id;
}
);
$args = array( 'force_delete' => $force_delete );
if ( null !== $suppress ) {
$args['suppress_filters'] = $suppress;
}
$order = OrderHelper::create_order();
$order_id = $order->get_id();
$sut = new WC_Order_Data_Store_CPT();
$sut->delete( $order, $args );
if ( true === $suppress ) {
$this->assertNull( $order_id_from_before_delete );
$this->assertNull( $order_id_from_after_delete );
$this->assertNull( $order_from_before_delete );
} else {
$this->assertEquals( $order_id, $order_id_from_before_delete );
$this->assertEquals( $order_id, $order_id_from_after_delete );
$this->assertSame( $order, $order_from_before_delete );
}
}
}

View File

@ -14,7 +14,7 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
$this->setup_cot();
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
}
/**
@ -29,7 +29,7 @@ class WC_User_Functions_Tests extends WC_Unit_Test_Case {
* Test wc_get_customer_order_count. Borrowed from `WC_Tests_Customer_Functions` class for COT.
*/
public function test_hpos_wc_customer_bought_product() {
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$customer_id_1 = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' );
$customer_id_2 = wc_create_new_customer( 'test2@example.com', 'testuser2', 'testpassword2' );
$product_1 = new WC_Product_Simple();

View File

@ -41,7 +41,7 @@ class EditLockTest extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
$this->setup_cot();
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order = new \WC_Order();

View File

@ -24,7 +24,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
public function setUp(): void {
parent::setUp();
$this->setup_cot();
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
$this->user_admin = $this->factory->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $this->user_admin );
@ -58,7 +58,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
$screen->post_type = 'post';
$this->assertFalse( $controller->is_order_screen() );
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
global $pagenow, $plugin_page;
$controller = new PageController();
@ -89,7 +89,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
$screen->base = 'post';
$this->assertFalse( $controller->is_order_screen( 'shop_order', 'list' ) );
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
global $pagenow, $plugin_page;
$controller = new PageController();
@ -124,7 +124,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
$mock_filter_input = false;
$this->assertFalse( $controller->is_order_screen( 'shop_order', 'edit' ) );
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
global $pagenow, $plugin_page;
$controller = new PageController();
@ -157,7 +157,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin\Orders {
$screen->action = '';
$this->assertFalse( $controller->is_order_screen( 'shop_order', 'new' ) );
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
global $pagenow, $plugin_page;
$controller = new PageController();

View File

@ -5,13 +5,17 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
require_once __DIR__ . '/../../../../helpers/HPOSToggleTrait.php';
/**
* Tests for DataSynchronizer class.
*/
class DataSynchronizerTests extends WC_Unit_Test_Case {
class DataSynchronizerTests extends HposTestCase {
use ArraySubsetAsserts;
use HPOSToggleTrait;
/**
* @var DataSynchronizer
@ -28,7 +32,7 @@ class DataSynchronizerTests extends WC_Unit_Test_Case {
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
OrderHelper::delete_order_custom_tables(); // We need this since non-temporary tables won't drop automatically.
OrderHelper::create_order_custom_table_if_not_exist();
OrderHelper::toggle_cot( false );
OrderHelper::toggle_cot_feature_and_usage( false );
$this->sut = wc_get_container()->get( DataSynchronizer::class );
$features_controller = wc_get_container()->get( Featurescontroller::class );
$features_controller->change_feature_enable( 'custom_order_tables', true );
@ -199,10 +203,8 @@ class DataSynchronizerTests extends WC_Unit_Test_Case {
/**
* When sync is enabled, and an order is deleted either from the post table or the COT table, the
* change should propagate across to the other table.
*
* @return void
*/
public function test_order_deletions_propagate(): void {
public function test_order_deletions_propagate_with_sync_enabled(): void {
// Sync enabled and COT authoritative.
update_option( $this->sut::ORDERS_DATA_SYNC_ENABLED_OPTION, 'yes' );
update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' );
@ -225,6 +227,187 @@ class DataSynchronizerTests extends WC_Unit_Test_Case {
);
}
/**
* @testdox When sync is disabled and the posts table is authoritative, deleting an order generates a deletion record in the meta table if the order exists in the backup table.
*
* @testWith [true]
* [false]
*
* @param bool $manual_sync True to trigger synchronization manually, false if automatic synchronization is enabled.
*/
public function test_synced_order_deletion_with_sync_disabled_and_posts_authoritative_generates_proper_deletion_record_if_cot_record_exists( bool $manual_sync ) {
$this->toggle_cot_authoritative( false );
if ( $manual_sync ) {
$this->disable_cot_sync();
$order = OrderHelper::create_order();
$this->do_cot_sync();
} else {
$this->enable_cot_sync();
$order = OrderHelper::create_order();
}
$this->disable_cot_sync();
$order_id = $order->get_id();
$order->delete( true );
$this->assert_deletion_record_existence( $order_id, false );
$this->assert_order_record_existence( $order_id, true, true );
}
/**
* @testdox When sync is disabled and the posts table is authoritative, deleting an order generates NO deletion record in the meta table if the order does NOT exist in the backup table.
*
* @return void
*/
public function test_synced_order_deletion_with_sync_disabled_and_posts_authoritative_not_generating_deletion_record_if_cot_record_not_exists() {
$this->toggle_cot_authoritative( false );
$this->disable_cot_sync();
$order = OrderHelper::create_order();
$order_id = $order->get_id();
$order->delete( true );
$this->assert_deletion_record_existence( $order_id, null, false );
}
/**
* @testdox 'get_next_batch_to_process' returns ids of deleted orders, together with orders pending to be created or updated.
*
* @testWith [true]
* [false]
*
* @param bool $cot_is_authoritative True to test with the orders table as authoritative, false to test with the posts table as authoritative.
*/
public function test_get_next_batch_to_process_returns_orders_deleted_from_current_authoritative_table( bool $cot_is_authoritative ) {
global $wpdb;
$this->toggle_cot_authoritative( $cot_is_authoritative );
$this->enable_cot_sync();
$order_1 = OrderHelper::create_order();
$order_2 = OrderHelper::create_order();
$order_3 = OrderHelper::create_order();
$order_4 = OrderHelper::create_order();
$this->disable_cot_sync();
$order_5 = OrderHelper::create_order();
$this->set_order_as_updated( $order_1, '2999-12-31 23:59:59', $cot_is_authoritative );
$order_3_id = $order_3->get_id();
$order_3->delete( true );
$order_4_id = $order_4->get_id();
$order_4->delete( true );
$this->assert_deletion_record_existence( $order_3_id, $cot_is_authoritative );
$this->assert_deletion_record_existence( $order_4_id, $cot_is_authoritative );
$batch = $this->sut->get_next_batch_to_process( 100 );
$this->assertEquals( array( $order_5->get_id(), $order_1->get_id(), $order_3_id, $order_4_id ), $batch );
}
/**
* @testdox 'process_batch' processes both orders pending creation/modification and orders pending deletion.
*
* @testWith [true]
* [false]
*
* @param bool $cot_is_authoritative True to test with the orders table as authoritative, false to test with the posts table as authoritative.
*/
public function test_process_batch_processes_modified_and_deleted_orders( bool $cot_is_authoritative ) {
$this->toggle_cot_authoritative( $cot_is_authoritative );
$this->enable_cot_sync();
$order_1 = OrderHelper::create_order();
$order_2 = OrderHelper::create_order();
$order_3 = OrderHelper::create_order();
$order_4 = OrderHelper::create_order();
$this->disable_cot_sync();
$order_1->set_date_modified( '2100-01-01 00:00:00' );
$order_1->save();
$order_3_id = $order_3->get_id();
$order_3->delete( true );
$order_4_id = $order_4->get_id();
$order_4->delete( true );
$this->sut->process_batch( array( $order_1->get_id(), $order_3_id, $order_4_id ) );
$this->assertEmpty( $this->sut->get_next_batch_to_process( 100 ) );
$this->assert_order_record_existence( $order_3_id, true, false );
$this->assert_order_record_existence( $order_3_id, false, false );
$this->assert_order_record_existence( $order_4_id, true, false );
$this->assert_order_record_existence( $order_4_id, false, false );
$this->assert_deletion_record_existence( $order_3_id, null, false );
$this->assert_deletion_record_existence( $order_4_id, null, false );
}
/**
* @testdox When an order deletion is recorded for an order that no longer exists in the backup table, a warning is logged.
*
* @testWith [true]
* [false]
*
* @param bool $cot_is_authoritative True to test with the orders table as authoritative, false to test with the posts table as authoritative.
*/
public function test_deletion_record_for_non_existing_order_logs_warning_on_sync( bool $cot_is_authoritative ) {
global $wpdb;
//phpcs:disable Squiz.Commenting
$logger = new class() {
public $warnings = array();
public function debug( $text ) {}
public function warning( $text ) {
$this->warnings[] = $text; }
public function error( $text ) {}
};
//phpcs:enable Squiz.Commenting
$this->reset_container_resolutions();
$this->reset_legacy_proxy_mocks();
$this->register_legacy_proxy_function_mocks(
array(
'wc_get_logger' => function() use ( $logger ) {
return $logger;
},
)
);
$this->sut = wc_get_container()->get( DataSynchronizer::class );
$this->toggle_cot_authoritative( $cot_is_authoritative );
$this->enable_cot_sync();
$order_1 = OrderHelper::create_order();
$order_2 = OrderHelper::create_order();
$this->disable_cot_sync();
$order_1_id = $order_1->get_id();
$order_1->delete( true );
$order_2_id = $order_2->get_id();
$order_2->delete( true );
if ( $cot_is_authoritative ) {
$wpdb->delete( $wpdb->posts, array( 'ID' => $order_1_id ) );
} else {
$wpdb->delete( OrdersTableDataStore::get_orders_table_name(), array( 'ID' => $order_1_id ) );
}
$this->sut->process_batch( array( $order_1_id, $order_2_id ) );
$this->assertEquals( array( "Order {$order_1_id} doesn't exist in the backup table, thus it can't be deleted" ), $logger->warnings );
}
/**
* When sync is enabled, changes to meta data should propagate from the Custom Orders Table to
* the post meta table whenever the order object's save_meta_data() method is called.
@ -319,7 +502,7 @@ class DataSynchronizerTests extends WC_Unit_Test_Case {
* @return void
*/
public function test_auto_draft_deletion(): void {
OrderHelper::toggle_cot( true );
OrderHelper::toggle_cot_feature_and_usage( true );
$order1 = new \WC_Order();
$order1->set_status( 'auto-draft' );

View File

@ -0,0 +1,125 @@
<?php
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Base class for HPOS related unit test suites.
*/
class HposTestCase extends WC_Unit_Test_Case {
/**
* Assert that a given order record exists or doesn't exist.
*
* @param int $order_id The order id to check.
* @param bool $in_cot True to assert that the order exists or not in the orders table, false to check in the posts table.
* @param bool $must_exist True to assert that the order exists, false to check that the order doesn't exist.
* @param string $order_type Expected order type, null to accept any type that starts with "shop_order".
* @return void
*/
protected function assert_order_record_existence( $order_id, $in_cot, $must_exist, $order_type = null ) {
global $wpdb;
$table_name = $in_cot ? OrdersTableDataStore::get_orders_table_name() : $wpdb->posts;
$order_type = $order_type ?? 'shop_order%';
$sql = $in_cot ?
"SELECT EXISTS (SELECT id FROM $table_name WHERE id = %d)" :
"SELECT EXISTS (SELECT ID FROM $table_name WHERE ID = %d AND post_type LIKE %s)";
//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$exists = $wpdb->get_var(
$in_cot ?
$wpdb->prepare( $sql, $order_id ) :
$wpdb->prepare( $sql, $order_id, $order_type )
);
//phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
if ( $must_exist ) {
$this->assertTrue( (bool) $exists, "No order found with id $order_id in table $table_name" );
} else {
$this->assertFalse( (bool) $exists, "Unexpected order found with id $order_id in table $table_name" );
}
}
/**
* Assert that an order deletion record exists or doesn't exist in the orders meta table.
*
* @param int $order_id The order id to check.
* @param bool $deleted_from_cot True to assert that the record corresponds to an order deleted from the orders table, or from the posts table otherwise.
* @param bool $must_exist True to assert that the record exists, false to assert that the record doesn't exist.
* @return void
*/
protected function assert_deletion_record_existence( $order_id, $deleted_from_cot, $must_exist = true ) {
global $wpdb;
$meta_table_name = OrdersTableDataStore::get_meta_table_name();
//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$record = $wpdb->get_row(
$wpdb->prepare(
"SELECT meta_value FROM $meta_table_name WHERE order_id = %d AND meta_key = %s",
$order_id,
DataSynchronizer::DELETED_RECORD_META_KEY
),
ARRAY_A
);
//phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( $must_exist ) {
$this->assertNotNull( $record, "No deletion record found for order id {$order_id}" );
} else {
$this->assertNull( $record, "Unexpected deletion record found for order id {$order_id}" );
return;
}
$deleted_from = $deleted_from_cot ?
DataSynchronizer::DELETED_FROM_ORDERS_META_VALUE :
DataSynchronizer::DELETED_FROM_POSTS_META_VALUE;
$this->assertEquals( $deleted_from, $record['meta_value'], "Deletion record for order {$order_id} has a value of {$record['meta_value']}, expected {$deleted_from}" );
}
/**
* Synchronize all the pending unsynchronized orders.
*/
protected function do_cot_sync() {
$sync = wc_get_container()->get( DataSynchronizer::class );
$batch = $sync->get_next_batch_to_process( 100 );
$sync->process_batch( $batch );
}
/**
* Sets an order as updated by directly modifying its "last updated gmt" field in the database.
*
* @param \WC_Order|int $order_or_id An order or an order id.
* @param string|null $update_date The new value for the "last updated gmt" in the database, defaults to the current date and time.
* @param bool|null $cot_is_authoritative Whether the orders table is authoritative or not. If null, it will be determined using OrderUtil.
* @return void
*/
protected function set_order_as_updated( $order_or_id, ?string $update_date = null, ?bool $cot_is_authoritative = null ) {
global $wpdb;
$order_id = $order_or_id instanceof \WC_Order ? $order_or_id->get_id() : $order_or_id;
$update_date = $update_date ?? current_time( 'mysql' );
$cot_is_authoritative = $cot_is_authoritative ?? OrderUtil::custom_orders_table_usage_is_enabled();
if ( $cot_is_authoritative ) {
$wpdb->update(
OrdersTableDataStore::get_orders_table_name(),
array(
'date_updated_gmt' => $update_date,
),
array( 'id' => $order_id )
);
} else {
$wpdb->update(
$wpdb->posts,
array(
'post_modified_gmt' => $update_date,
),
array( 'id' => $order_id )
);
}
}
}

View File

@ -29,14 +29,14 @@ class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Cont
OrderHelper::delete_order_custom_tables();
OrderHelper::create_order_custom_table_if_not_exist();
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
}
/**
* Destroys system under test.
*/
public function tearDown(): void {
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
// Add back removed filter.
add_filter( 'query', array( $this, '_create_temporary_tables' ) );
@ -51,7 +51,7 @@ class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Cont
public function test_orders_cpt() {
wp_set_current_user( $this->user );
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
$post_order_id = OrderHelper::create_complex_wp_post_order();
( wc_get_container()->get( PostsToOrdersMigrationController::class ) )->migrate_orders( array( $post_order_id ) );
@ -62,7 +62,7 @@ class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Cont
$response_cpt_data = $response_cpt->get_data();
// Re-enable COT.
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$response_cot = $this->server->dispatch( $request );
$this->assertEquals( 200, $response_cot->get_status() );
@ -91,7 +91,7 @@ class OrdersTableDataStoreRestOrdersControllerTests extends \WC_REST_Orders_Cont
* @param boolean $enabled TRUE to enable COT or FALSE to disable.
* @return void
*/
private function toggle_cot( bool $enabled ): void {
private function toggle_cot_feature_and_usage( bool $enabled ): void {
$features_controller = wc_get_container()->get( Featurescontroller::class );
$features_controller->change_feature_enable( 'custom_order_tables', true );

View File

@ -14,7 +14,7 @@ use Automattic\WooCommerce\Utilities\OrderUtil;
*
* Test for OrdersTableDataStore class.
*/
class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
class OrdersTableDataStoreTests extends HposTestCase {
use HPOSToggleTrait;
/**
@ -56,11 +56,11 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
// Remove the Test Suites use of temporary tables https://wordpress.stackexchange.com/a/220308.
$this->setup_cot();
$this->cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
$container = wc_get_container();
$container->reset_all_resolved();
$this->sut = $container->get( OrdersTableDataStore::class );
$this->migrator = $container->get( PostsToOrdersMigrationController::class );
$this->sut = wc_get_container()->get( OrdersTableDataStore::class );
$this->migrator = wc_get_container()->get( PostsToOrdersMigrationController::class );
$this->cpt_data_store = new WC_Order_Data_Store_CPT();
}
@ -70,7 +70,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
public function tearDown(): void {
//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', $this->original_time_zone );
$this->toggle_cot( $this->cot_state );
$this->toggle_cot_feature_and_usage( $this->cot_state );
$this->clean_up_cot_setup();
parent::tearDown();
}
@ -217,7 +217,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
wp_cache_flush();
$order = new WC_Order();
$order->set_id( $post_order->get_id() );
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$this->switch_data_store( $order, $this->sut );
$this->sut->read( $order );
@ -252,7 +252,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
foreach ( $datastore_updates as $prop => $value ) {
$this->assertEquals( $value, $this->sut->{"get_$prop"}( $order ), "Unable to match prop $prop" );
}
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
}
/**
@ -740,7 +740,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
* @testDox Tests queries involving 'orderby' and meta queries.
*/
public function test_cot_query_meta_orderby() {
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order1 = new \WC_Order();
$order1->add_meta_data( 'color', 'red' );
@ -1781,7 +1781,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
* Ideally, this should be possible only from getters and setters for objects, but for backward compatibility, earlier ways are also supported.
*/
public function test_internal_ds_getters_and_setters() {
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$props_to_test = array(
'_download_permissions_granted',
'_recorded_sales',
@ -1828,7 +1828,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$order->save();
}
$this->assert_get_prop_via_ds_object_and_metadata( $props_to_test, $order, false, $ds_getter_setter_names );
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
}
/**
@ -1871,7 +1871,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
* @testDox Legacy getters and setters for props migrated from data stores should be set/reset properly.
*/
public function test_legacy_getters_setters() {
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order_id = OrderHelper::create_complex_data_store_order( $this->sut );
$order = wc_get_order( $order_id );
$this->switch_data_store( $order, $this->sut );
@ -1904,7 +1904,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$this->assert_props_value_via_data_store( $order, $bool_props, true );
$this->assert_props_value_via_order_object( $order, $bool_props, true );
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
}
/**
@ -2000,7 +2000,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
* @testDox Test that multiple calls to read don't try to sync again.
*/
public function test_read_multiple_dont_sync_again_for_same_order() {
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order = $this->create_complex_cot_order();
$this->sut->backfill_post_record( $order );
$this->enable_cot_sync();
@ -2017,7 +2017,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$this->assertTrue( $should_sync_callable->call( $this->sut, $order ) );
$this->sut->read_multiple( $orders );
$this->assertFalse( $should_sync_callable->call( $this->sut, $order ) );
$this->toggle_cot( false );
$this->toggle_cot_feature_and_usage( false );
}
/**
@ -2032,7 +2032,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
)
);
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order = new WC_Order();
$order->save();
@ -2059,7 +2059,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
)
);
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order = new WC_Order();
$order->save();
@ -2074,12 +2074,11 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$this->assertFalse( $child_order );
}
/**
* @testDox Make sure get_order return false when checking an order of different order types without warning.
*/
public function test_get_order_with_id_for_different_type() {
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$this->disable_cot_sync();
$product = new \WC_Product();
$product->save();
@ -2108,7 +2107,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
*/
public function test_address_index_saved_on_update() {
global $wpdb;
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$this->disable_cot_sync();
$order = new WC_Order();
$order->set_billing_address_1( '123 Main St' );
@ -2131,6 +2130,187 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$this->assertEquals( 1, $result );
}
/**
* @testdox When sync is enabled and an order is deleted, records in both the authoritative and the backup tables are deleted, and no deletion records are created.
*
* @testWith [true]
* [false]
*
* @param bool $cot_is_authoritative True to test with the orders table as authoritative, false to test with the posts table as authoritative.
*/
public function test_order_deletion_with_sync_enabled( bool $cot_is_authoritative ) {
$this->allow_current_user_to_delete_posts();
$this->toggle_cot_feature_and_usage( true );
$this->toggle_cot_authoritative( $cot_is_authoritative );
$this->enable_cot_sync();
list($order, $refund) = $this->create_order_with_refund();
$order_id = $order->get_id();
$refund_id = $refund->get_id();
$order->delete( true );
$this->assert_no_order_records_or_deletion_records_exist( $order_id, $refund_id, $cot_is_authoritative );
}
/**
* @testdox Deletion records are created when an order is deleted with sync disabled, then when sync is enabled all order and deletion records are deleted.
*
* @testWith [true]
* [false]
*
* @param bool $cot_is_authoritative True to test with the orders table as authoritative, false to test with the posts table as authoritative.
*/
public function test_order_deletion_with_sync_disabled( bool $cot_is_authoritative ) {
$this->allow_current_user_to_delete_posts();
$this->toggle_cot_feature_and_usage( true );
$this->toggle_cot_authoritative( $cot_is_authoritative );
$this->enable_cot_sync();
list($order, $refund) = $this->create_order_with_refund();
$order_id = $order->get_id();
$refund_id = $refund->get_id();
$this->disable_cot_sync();
$order->delete( true );
$this->assert_order_record_existence( $order_id, true, ! $cot_is_authoritative );
$this->assert_order_record_existence( $order_id, false, $cot_is_authoritative );
$this->assert_order_record_existence( $refund_id, true, ! $cot_is_authoritative );
$this->assert_order_record_existence( $refund_id, false, $cot_is_authoritative );
$this->assert_deletion_record_existence( $order_id, $cot_is_authoritative, true );
$this->assert_deletion_record_existence( $refund_id, $cot_is_authoritative, true );
$this->do_cot_sync();
$this->assert_no_order_records_or_deletion_records_exist( $order_id, $refund_id, $cot_is_authoritative );
}
/**
* @testdox When the orders table is authoritative, sync is disabled and an order is deleted, existing placeholder records are deleted from the posts table.
*/
public function test_order_deletion_with_sync_disabled_when_placeholders_are_created() {
$this->allow_current_user_to_delete_posts();
$this->toggle_cot_feature_and_usage( true );
$this->toggle_cot_authoritative( true );
$this->disable_cot_sync();
list($order, $refund) = $this->create_order_with_refund();
$order_id = $order->get_id();
$refund_id = $refund->get_id();
$this->assert_order_record_existence( $order_id, true, true );
$this->assert_order_record_existence( $order_id, false, true, 'shop_order_placehold' );
$this->assert_order_record_existence( $refund_id, true, true );
$this->assert_order_record_existence( $refund_id, false, true, 'shop_order_placehold' );
$order->delete( true );
$this->assert_no_order_records_or_deletion_records_exist( $order_id, $refund_id, false );
}
/**
* @testdox When deleting an order whose associated post type is hierarchical, child orders aren't deleted and get the parent id of their parent order.
*/
public function test_order_deletion_when_post_type_is_hierarchical_results_in_child_order_upshifting() {
$this->reset_container_resolutions();
$this->reset_legacy_proxy_mocks();
$this->register_legacy_proxy_function_mocks(
array(
'is_post_type_hierarchical' => function( $post_type ) {
return 'shop_order' === $post_type ? true : is_post_type_hierarchical( $post_type );
},
)
);
$this->sut = wc_get_container()->get( OrdersTableDataStore::class );
$this->allow_current_user_to_delete_posts();
$this->toggle_cot_feature_and_usage( true );
$this->toggle_cot_authoritative( true );
$this->disable_cot_sync();
list($order, $refund) = $this->create_order_with_refund();
$order_id = $order->get_id();
$refund_id = $refund->get_id();
$this->switch_data_store( $order, $this->sut );
$order2 = OrderHelper::create_order();
$order2_id = $order2->get_id();
$order->set_parent_id( $order2_id );
$order->save();
$order->delete( true );
$this->assert_order_record_existence( $order_id, true, false );
$this->assert_order_record_existence( $refund_id, true, true );
$refund = wc_get_order( $refund_id );
$this->assertEquals( $order2_id, $refund->get_parent_id() );
}
/**
* Mock the current user capabilities so that it's allowed to delete posts.
*
* @return void
*/
private function allow_current_user_to_delete_posts() {
$this->register_legacy_proxy_function_mocks(
array(
'current_user_can' => function( $capability ) {
return 'delete_posts' === $capability ? true : current_user_can( $capability );
},
)
);
}
/**
* Assert than no records exist whatsoever, and no deletion records either, for a given order and for its refund.
*
* @param int $order_id The order id to test.
* @param int $refund_id The refund id to test.
* @param bool $cot_is_authoritative True if the deletion record existence is to be checked for the orders table, false for the posts table.
* @return void
*/
private function assert_no_order_records_or_deletion_records_exist( $order_id, $refund_id, $cot_is_authoritative ) {
$this->assert_order_record_existence( $order_id, true, false );
$this->assert_order_record_existence( $order_id, false, false );
$this->assert_order_record_existence( $refund_id, true, false );
$this->assert_order_record_existence( $refund_id, false, false );
$this->assert_deletion_record_existence( $order_id, $cot_is_authoritative, false );
$this->assert_deletion_record_existence( $refund_id, $cot_is_authoritative, false );
}
/**
* Create an order and a refund.
*
* @return array An array containing the order as the first element and the refund as the second element.
*/
private function create_order_with_refund() {
$order = OrderHelper::create_order();
$item = current( $order->get_items() )->get_data();
$refund = wc_create_refund(
array(
'order_id' => $order->get_id(),
'line_items' => array(
$item['id'] => array(
'id' => $item['id'],
'qty' => $item['quantity'],
'refund_total' => $item['total'],
'refund_tax' => $item['total_tax'],
),
),
)
);
$refund->save();
return array( $order, $refund );
}
/**
* @testDox When saving an order, status is automatically prefixed even if it was not earlier.
*/
@ -2157,7 +2337,7 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
public function test_error_when_setting_order_property_is_captured_and_logged( bool $orders_authoritative ) {
global $wpdb;
$this->toggle_cot( $orders_authoritative );
$this->toggle_cot_feature_and_usage( $orders_authoritative );
$this->disable_cot_sync();
// phpcs:disable Squiz.Commenting
@ -2226,4 +2406,66 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$this->assertEquals( 0, $refund->get_parent_id() );
$this->assertEquals( "Error when setting property 'parent_id' for order {$refund->get_id()}: Invalid parent ID", current( $fake_logger->warnings )['message'] );
}
/**
* @testDox A 'suppress_filters' argument can be passed to 'delete', if true no 'woocommerce_(before_)trash/delete_order' actions will be fired.
*
* @testWith [null, true]
* [true, true]
* [false, true]
* [null, false]
* [true, false]
* [false, false]
*
* @param bool|null $suppress True or false to use a 'suppress_filters' argument with that value, null to not use it.
* @param bool $force_delete True to delete the order, false to trash it.
* @return void
*/
public function test_filters_can_be_suppressed_when_trashing_or_deleting_an_order( ?bool $suppress, bool $force_delete ) {
$order_id_from_before_delete = null;
$order_id_from_after_delete = null;
$order_from_before_delete = null;
$this->toggle_cot_feature_and_usage( true );
$this->disable_cot_sync();
$trash_or_delete = $force_delete ? 'delete' : 'trash';
add_action(
"woocommerce_before_{$trash_or_delete}_order",
function ( $order_id, $order ) use ( &$order_id_from_before_delete, &$order_from_before_delete ) {
$order_id_from_before_delete = $order_id;
$order_from_before_delete = $order;
},
10,
2
);
add_action(
"woocommerce_{$trash_or_delete}_order",
function ( $order_id ) use ( &$order_id_from_after_delete ) {
$order_id_from_after_delete = $order_id;
}
);
$args = array( 'force_delete' => $force_delete );
if ( null !== $suppress ) {
$args['suppress_filters'] = $suppress;
}
$order = OrderHelper::create_order();
$order_id = $order->get_id();
$this->sut->delete( $order, $args );
if ( true === $suppress ) {
$this->assertNull( $order_id_from_before_delete );
$this->assertNull( $order_id_from_after_delete );
$this->assertNull( $order_from_before_delete );
} else {
$this->assertEquals( $order_id, $order_id_from_before_delete );
$this->assertEquals( $order_id, $order_id_from_after_delete );
$this->assertSame( $order, $order_from_before_delete );
}
}
}

View File

@ -24,14 +24,14 @@ class OrdersTableQueryTests extends WC_Unit_Test_Case {
parent::setUp();
$this->setup_cot();
$this->cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
}
/**
* Restore the original COT state.
*/
public function tearDown(): void {
$this->toggle_cot( $this->cot_state );
$this->toggle_cot_feature_and_usage( $this->cot_state );
parent::tearDown();
}

View File

@ -79,7 +79,7 @@ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case {
*/
public function test_refunds_backfill() {
$this->enable_cot_sync();
$this->toggle_cot( true );
$this->toggle_cot_feature_and_usage( true );
$order = OrderHelper::create_complex_data_store_order( $this->order_data_store );
$refund = wc_create_refund(
array(

View File

@ -40,7 +40,7 @@ class COTMigrationUtilTest extends WC_Unit_Test_Case {
* @return void
*/
public function tearDown(): void {
OrderHelper::toggle_cot( $this->prev_cot_state );
OrderHelper::toggle_cot_feature_and_usage( $this->prev_cot_state );
parent::tearDown();
}
@ -125,7 +125,7 @@ class COTMigrationUtilTest extends WC_Unit_Test_Case {
public function test_get_table_for_orders_posts() {
global $wpdb;
OrderHelper::toggle_cot( false );
OrderHelper::toggle_cot_feature_and_usage( false );
$table_name = $this->sut->get_table_for_orders();
$this->assertEquals( $wpdb->posts, $table_name );
@ -137,7 +137,7 @@ class COTMigrationUtilTest extends WC_Unit_Test_Case {
public function test_get_table_for_orders_hpos() {
global $wpdb;
OrderHelper::toggle_cot( true );
OrderHelper::toggle_cot_feature_and_usage( true );
$table_name = $this->sut->get_table_for_orders();
$this->assertEquals( "{$wpdb->prefix}wc_orders", $table_name );
@ -149,7 +149,7 @@ class COTMigrationUtilTest extends WC_Unit_Test_Case {
public function test_get_table_for_order_meta_posts() {
global $wpdb;
OrderHelper::toggle_cot( false );
OrderHelper::toggle_cot_feature_and_usage( false );
$table_name = $this->sut->get_table_for_order_meta();
$this->assertEquals( $wpdb->postmeta, $table_name );
@ -161,7 +161,7 @@ class COTMigrationUtilTest extends WC_Unit_Test_Case {
public function test_get_table_for_order_meta_hpos() {
global $wpdb;
OrderHelper::toggle_cot( true );
OrderHelper::toggle_cot_feature_and_usage( true );
$table_name = $this->sut->get_table_for_order_meta();
$this->assertEquals( "{$wpdb->prefix}wc_orders_meta", $table_name );

View File

@ -110,4 +110,40 @@ class StringUtilTest extends \WC_Unit_Test_Case {
$result = StringUtil::is_null_or_whitespace( $value );
$this->assertEquals( $expected, $result );
}
/**
* @testDox 'to_sql_list' generates a parenthesized and comma-separated list of the passed values.
*/
public function test_to_sql_list() {
$result = StringUtil::to_sql_list( array( 34 ) );
$this->assertEquals( '(34)', $result );
$result = StringUtil::to_sql_list( array( 34, 'foo', true ) );
$this->assertEquals( '(34,foo,1)', $result );
}
/**
* @testDox 'to_sql_list' throws an exception if an empty array is passed.
*/
public function test_to_sql_list_with_empty_input() {
$this->expectException( \InvalidArgumentException::class );
$this->expectExceptionMessage( 'StringUtil::to_sql_list: the values array is empty' );
StringUtil::to_sql_list( array() );
}
/**
* @testDox 'class_name_without_namespace' returns what its name says.
*
* @testWith ["foo\\bar\\fizz", "fizz"]
* ["foo", "foo"]
* ["", ""]
*
* @param string $input The string to test.
* @param string $expected_output The expected output.
*/
public function test_class_name_without_namespace( string $input, string $expected_output ) {
$actual_output = StringUtil::class_name_without_namespace( $input );
$this->assertEquals( $expected_output, $actual_output );
}
}