Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Initial exploration of compatibility with WC HPOS #4038

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions includes/classes/Feature/WooCommerce/Orders.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ class Orders {
*/
protected $woocommerce;

/**
* Receive the OrdersHPOS object instance
*
* @var OrdersHPOS
*/
protected $orders_hpos;

/**
* Class constructor
*
* @param WooCommerce $woocommerce WooCommerce feature object instance
*/
public function __construct( WooCommerce $woocommerce ) {
$this->woocommerce = $woocommerce;
$this->orders_hpos = new OrdersHPOS( $this );
}

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

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

/**
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
*
Expand Down
207 changes: 207 additions & 0 deletions includes/classes/Feature/WooCommerce/OrdersHPOS.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php
/**
* WooCommerce HPOS compatibility layer
*
* @since 5.3.0
* @package elasticpress
*/

namespace ElasticPress\Feature\WooCommerce;

use ElasticPress\Indexables;

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* WooCommerce HPOS
*/
class OrdersHPOS {
/**
* Orders object instance
*
* @var Orders
*/
protected $orders;

/**
* Class constructor
*
* @param Orders $orders Orders object instance
*/
public function __construct( Orders $orders ) {
$this->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;
}
}
34 changes: 29 additions & 5 deletions tests/php/features/WooCommerce/TestWooCommerceOrders.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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 );
}
}