diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 39411f78d..3b171d948 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -25,6 +25,13 @@ class Orders { */ protected $woocommerce; + /** + * Receive the OrdersHPOS object instance + * + * @var OrdersHPOS + */ + protected $orders_hpos; + /** * Class constructor * @@ -32,6 +39,7 @@ class Orders { */ public function __construct( WooCommerce $woocommerce ) { $this->woocommerce = $woocommerce; + $this->orders_hpos = new OrdersHPOS( $this ); } /** @@ -46,6 +54,10 @@ public function setup() { add_action( 'parse_query', [ $this, 'search_order' ], 11 ); add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); add_filter( 'ep_admin_notices', [ $this, 'hpos_compatibility_notice' ] ); + + if ( $this->is_hpos_enabled() ) { + $this->orders_hpos->setup(); + } } /** @@ -61,6 +73,10 @@ public function tear_down() { remove_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 ); remove_action( 'parse_query', [ $this, 'search_order' ], 11 ); remove_action( 'pre_get_posts', [ $this, 'translate_args' ], 11 ); + + if ( $this->is_hpos_enabled() ) { + $this->orders_hpos->tear_down(); + } } /** @@ -359,13 +375,7 @@ public function hpos_compatibility_notice( array $notices ): array { return $notices; } - if ( - ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) - || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) { - return $notices; - } - - if ( ! \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { + if ( ! $this->is_hpos_enabled() ) { return $notices; } @@ -480,6 +490,22 @@ public function translate_args( $query ) { $this->maybe_set_search_fields( $query ); } + /** + * Whether WooCommerce HPOS is enabled or not + * + * @since 5.3.0 + * @return boolean Whether WooCommerce HPOS is enabled or not + */ + public function is_hpos_enabled(): bool { + if ( + ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) + || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) { + return false; + } + + return \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); + } + /** * Handle calls to OrdersAutosuggest methods * diff --git a/includes/classes/Feature/WooCommerce/OrdersHPOS.php b/includes/classes/Feature/WooCommerce/OrdersHPOS.php new file mode 100644 index 000000000..366c0b876 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/OrdersHPOS.php @@ -0,0 +1,207 @@ +orders = $orders; + } + + /** + * Setup order HPOS related hooks + */ + public function setup() { + add_action( 'woocommerce_new_order', [ $this, 'sync_order' ] ); + add_action( 'woocommerce_refund_created', [ $this, 'sync_order' ] ); + add_action( 'woocommerce_update_order', [ $this, 'sync_order' ] ); + add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'set_order_data' ], 10, 2 ); + } + + /** + * Unsetup order HPOS related hooks + */ + public function tear_down() { + remove_action( 'woocommerce_new_order', [ $this, 'sync_order' ], 10, 2 ); + remove_action( 'woocommerce_refund_created', [ $this, 'sync_order' ], 10, 2 ); + remove_action( 'woocommerce_update_order', [ $this, 'sync_order' ], 10, 2 ); + remove_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'set_order_data' ], 10, 2 ); + } + + /** + * Add orders to the sync queue + * + * @param int $order_id Order ID. + */ + public function sync_order( $order_id ) { + Indexables::factory()->get( 'post' )->sync_manager->add_to_queue( $order_id ); + } + + /** + * Add order data to ES document args + * + * @param array $post_args Post arguments + * @param int $post_id Post ID + * @return array + */ + public function set_order_data( $post_args, $post_id ) { + if ( + \Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE !== $post_args['post_type'] + && ! in_array( $post_args['post_type'], $this->orders->get_supported_post_types(), true ) ) { + return $post_args; + } + + $post_indexable = Indexables::factory()->get( 'post' ); + $order = wc_get_order( $post_id ); + + $post_args['post_type'] = $order->get_type(); + $post_args['post_status'] = $order->get_status( 'edit' ); + $post_args['post_parent'] = $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0; + $post_args['post_date'] = gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ); + $post_args['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ); + $post_args['edit_date'] = true; + $post_args['post_excerpt'] = method_exists( $order, 'get_customer_note' ) ? $order->get_customer_note() : ''; + + $post_order = new \WP_Post( (object) $post_args ); + + add_filter( 'ep_prepare_meta_data', [ $this, 'prepare_meta_data' ], 10, 2 ); + $post_args['meta'] = $post_indexable->prepare_meta_types( $post_indexable->prepare_meta( $post_order ) ); + remove_filter( 'ep_prepare_meta_data', [ $this, 'prepare_meta_data' ] ); + + return $post_args; + } + + /** + * Get meta data from an order as it would be stored in the post_meta table. + * + * This method is a copy of WC_Order_Data_Store_CPT::update_post_meta() with some simplifications and returning data as an array, + * instead of actually storing it in the database. + * + * @param array $order_meta Meta data + * @param WP_Post $order_post Order object + * @return array + */ + public function prepare_meta_data( $order_meta, $order_post ) { + $data_store = new \WC_Order_Data_Store_CPT(); + $order = wc_get_order( $order_post->ID ); + + $meta_data = []; + $meta_key_to_props = [ + '_order_key' => 'order_key', + '_customer_user' => 'customer_id', + '_payment_method' => 'payment_method', + '_payment_method_title' => 'payment_method_title', + '_transaction_id' => 'transaction_id', + '_customer_ip_address' => 'customer_ip_address', + '_customer_user_agent' => 'customer_user_agent', + '_created_via' => 'created_via', + '_date_completed' => 'date_completed', + '_date_paid' => 'date_paid', + '_cart_hash' => 'cart_hash', + '_download_permissions_granted' => 'download_permissions_granted', + '_recorded_sales' => 'recorded_sales', + '_recorded_coupon_usage_counts' => 'recorded_coupon_usage_counts', + '_new_order_email_sent' => 'new_order_email_sent', + '_order_stock_reduced' => 'order_stock_reduced', + ]; + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'date_paid': + case 'date_completed': + $value = ! is_null( $value ) ? $value->getTimestamp() : ''; + break; + case 'download_permissions_granted': + case 'recorded_sales': + case 'recorded_coupon_usage_counts': + case 'order_stock_reduced': + if ( is_null( $value ) || '' === $value ) { + break; + } + $value = is_bool( $value ) ? wc_bool_to_string( $value ) : $value; + break; + case 'new_order_email_sent': + if ( is_null( $value ) || '' === $value ) { + break; + } + $value = is_bool( $value ) ? wc_bool_to_string( $value ) : $value; + $value = 'yes' === $value ? 'true' : 'false'; // For backward compatibility, we store as true/false in DB. + break; + } + + // We want to persist internal data store keys as 'yes' or 'no' if they are boolean to maintain compatibility. + if ( is_bool( $value ) && in_array( $prop, array_values( $data_store->get_internal_data_store_key_getters() ), true ) ) { + $value = wc_bool_to_string( $value ); + } + + $meta_data[ $meta_key ] = [ $value ]; + } + + $address_props = array( + 'billing' => array( + '_billing_first_name' => 'billing_first_name', + '_billing_last_name' => 'billing_last_name', + '_billing_company' => 'billing_company', + '_billing_address_1' => 'billing_address_1', + '_billing_address_2' => 'billing_address_2', + '_billing_city' => 'billing_city', + '_billing_state' => 'billing_state', + '_billing_postcode' => 'billing_postcode', + '_billing_country' => 'billing_country', + '_billing_email' => 'billing_email', + '_billing_phone' => 'billing_phone', + ), + 'shipping' => array( + '_shipping_first_name' => 'shipping_first_name', + '_shipping_last_name' => 'shipping_last_name', + '_shipping_company' => 'shipping_company', + '_shipping_address_1' => 'shipping_address_1', + '_shipping_address_2' => 'shipping_address_2', + '_shipping_city' => 'shipping_city', + '_shipping_state' => 'shipping_state', + '_shipping_postcode' => 'shipping_postcode', + '_shipping_country' => 'shipping_country', + '_shipping_phone' => 'shipping_phone', + ), + ); + + foreach ( $address_props as $props ) { + foreach ( $props as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + + $meta_data[ $meta_key ] = [ $value ]; + } + } + + return $meta_data; + } +} diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index 44608146f..466b4c794 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -348,11 +348,7 @@ public function test_hpos_compatibility_notice() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); $this->assertCount( 1, $this->orders->hpos_compatibility_notice( $notices ) ); - $option_name = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION; - $change_value = function () { - return 'yes'; - }; - add_filter( 'pre_option_' . $option_name, $change_value ); + $this->enable_hpos(); $new_notices = $this->orders->hpos_compatibility_notice( $notices ); $this->assertCount( 2, $new_notices ); @@ -370,4 +366,32 @@ public function test_hpos_compatibility_notice() { $this->assertCount( 1, $new_notices ); $this->assertArrayNotHasKey( 'wc_orders_incompatible', $new_notices ); } + + /** + * Test the `is_hpos_enabled` method + * + * @since 5.3.0 + * @group woocommerce + * @group woocommerce-orders + */ + public function test_is_hpos_enabled() { + $this->assertFalse( $this->orders->is_hpos_enabled() ); + + $this->enable_hpos(); + + $this->assertTrue( $this->orders->is_hpos_enabled() ); + } + + /** + * Utilitary function to enable WooCommerce HPOS + * + * @since 5.3.0 + */ + protected function enable_hpos() { + $option_name = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION; + $change_value = function () { + return 'yes'; + }; + add_filter( 'pre_option_' . $option_name, $change_value ); + } }