diff --git a/plugins/woocommerce/changelog/cot-fix-order-related-methods-in-customer-data-store b/plugins/woocommerce/changelog/cot-fix-order-related-methods-in-customer-data-store new file mode 100644 index 00000000000..1e930fe21da --- /dev/null +++ b/plugins/woocommerce/changelog/cot-fix-order-related-methods-in-customer-data-store @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Fix order related methods in customer data store diff --git a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php index 7bbfe8319db..5219aff2867 100644 --- a/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php +++ b/plugins/woocommerce/includes/data-stores/class-wc-customer-data-store.php @@ -5,6 +5,9 @@ * @package WooCommerce\DataStores */ +use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; + if ( ! defined( 'ABSPATH' ) ) { exit; } @@ -320,6 +323,15 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props ); } + /** + * Check if the usage of the custom orders table is enabled. + * + * @return bool + */ + private function is_cot_in_use(): bool { + return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled(); + } + /** * Gets the customers last order. * @@ -328,35 +340,59 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat * @return WC_Order|false */ public function get_last_order( &$customer ) { - $last_order = apply_filters( + //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment + /** + * Filters the id of the last order from a given customer. + * + * @param string @last_order_id The last order id as retrieved from the database. + * @param WC_Customer The customer whose last order id is being retrieved. + * @return string The actual last order id to use. + */ + $last_order_id = apply_filters( 'woocommerce_customer_get_last_order', get_user_meta( $customer->get_id(), '_last_order', true ), $customer ); + //phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment - if ( '' === $last_order ) { + if ( '' === $last_order_id ) { global $wpdb; - $last_order = $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - "SELECT posts.ID + $order_statuses_sql = "( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )"; + + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + if ( $this->is_cot_in_use() ) { + $sql = $wpdb->prepare( + 'SELECT id FROM ' . OrdersTableDataStore::get_orders_table_name() . " + WHERE customer_id = %d + AND status in $order_statuses_sql + ORDER BY id DESC + LIMIT 1", + $customer->get_id() + ); + $last_order_id = $wpdb->get_var( $sql ); + } else { + $last_order_id = $wpdb->get_var( + "SELECT posts.ID FROM $wpdb->posts AS posts LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) - ORDER BY posts.ID DESC" - // phpcs:enable - ); - update_user_meta( $customer->get_id(), '_last_order', $last_order ); + AND posts.post_status IN $order_statuses_sql + ORDER BY posts.ID DESC + LIMIT 1" + ); + } + //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + update_user_meta( $customer->get_id(), '_last_order', $last_order_id ); } - if ( ! $last_order ) { + if ( ! $last_order_id ) { return false; } - return wc_get_order( absint( $last_order ) ); + return wc_get_order( absint( $last_order_id ) ); } /** @@ -373,20 +409,33 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat $customer ); + $order_statuses_sql = "( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' )"; + if ( '' === $count ) { global $wpdb; - $count = $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - "SELECT COUNT(*) + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + if ( $this->is_cot_in_use() ) { + $sql = $wpdb->prepare( + 'SELECT COUNT(id) FROM ' . OrdersTableDataStore::get_orders_table_name() . " + WHERE customer_id = %d + AND status in $order_statuses_sql", + $customer->get_id() + ); + $count = $wpdb->get_var( $sql ); + } else { + $count = $wpdb->get_var( + "SELECT COUNT(*) FROM $wpdb->posts as posts LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id WHERE meta.meta_key = '_customer_user' AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + AND posts.post_status IN $order_statuses_sql AND meta_value = '" . esc_sql( $customer->get_id() ) . "'" - // phpcs:enable - ); + ); + } + //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + update_user_meta( $customer->get_id(), '_order_count', $count ); } @@ -410,24 +459,42 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat if ( '' === $spent ) { global $wpdb; - $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); - $spent = $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared - apply_filters( - 'woocommerce_customer_get_total_spent_query', - "SELECT SUM(meta2.meta_value) + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + $statuses_sql = "( 'wc-" . implode( "','wc-", $statuses ) . "' )"; + + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + if ( $this->is_cot_in_use() ) { + $sql = $wpdb->prepare( + 'SELECT SUM(total_amount) FROM ' . OrdersTableDataStore::get_orders_table_name() . " + WHERE customer_id = %d + AND status in $statuses_sql", + $customer->get_id() + ); + } else { + $sql = "SELECT SUM(meta2.meta_value) FROM $wpdb->posts as posts LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id WHERE meta.meta_key = '_customer_user' AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' AND posts.post_type = 'shop_order' - AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) - AND meta2.meta_key = '_order_total'", - $customer - ) - // phpcs:enable - ); + AND posts.post_status IN $statuses_sql + AND meta2.meta_key = '_order_total'"; + } + + //phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment + /** + * Filters the SQL query used to get the combined total of all the orders from a given customer. + * + * @param string The SQL query to use. + * @param WC_Customer The customer to get the total spent for. + * @return string The actual SQL query to use. + */ + $sql = apply_filters( 'woocommerce_customer_get_total_spent_query', $sql, $customer ); + //phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment + + $spent = $wpdb->get_var( $sql ); + //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( ! $spent ) { $spent = 0; @@ -511,7 +578,7 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat * @return array */ public function get_user_ids_for_billing_email( $emails ) { - $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); + $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); $users_query = new WP_User_Query( array( 'fields' => 'ID', diff --git a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php index 52db052a917..9c6dd3d16ab 100644 --- a/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php +++ b/plugins/woocommerce/tests/legacy/framework/helpers/class-wc-helper-order.php @@ -40,10 +40,11 @@ class WC_Helper_Order { * * @param int $customer_id The ID of the customer the order is for. * @param WC_Product $product The product to add to the order. + * @param array $order_data Order data to be passed to wc_create_order. * * @return WC_Order */ - public static function create_order( $customer_id = 1, $product = null ) { + public static function create_order( $customer_id = 1, $product = null, $order_data = array() ) { if ( ! is_a( $product, 'WC_Product' ) ) { $product = WC_Helper_Product::create_simple_product(); @@ -51,12 +52,13 @@ class WC_Helper_Order { WC_Helper_Shipping::create_simple_flat_rate(); - $order_data = array( + $default_order_data = array( 'status' => 'pending', 'customer_id' => $customer_id, 'customer_note' => '', 'total' => '', ); + $order_data = wp_parse_args( $order_data, $default_order_data ); $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; // Required, else wc_create_order throws an exception. $order = wc_create_order( $order_data ); diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php index aaa3817fa2b..61aed568b4a 100644 --- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php +++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-customer-data-store-test.php @@ -1,10 +1,22 @@ assertEquals( $customer_id, $customer->get_id() ); $this->assertEquals( $username, $customer->get_username() ); } + + /** + * Handler for the wc_order_statuses filter, returns just 'pending" as the valid order statuses list. + * + * @return string[] + */ + public function get_pending_only_as_order_statuses() { + return array( 'wc-pending' => 'pending' ); + } + + /** + * @testdox 'get_last_order' works when the posts table is used for storing orders. + */ + public function test_get_last_customer_order_not_using_cot() { + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + + $customer_1 = WC_Helper_Customer::create_customer( 'test1', 'pass1', 'test1@example.com' ); + $customer_2 = WC_Helper_Customer::create_customer( 'test2', 'pass2', 'test2@example.com' ); + WC_Helper_Order::create_order( $customer_1->get_id() ); + $last_valid_order_of_1 = WC_Helper_Order::create_order( $customer_1->get_id() ); + WC_Helper_Order::create_order( $customer_1->get_id(), null, array( 'status' => 'completed' ) ); + WC_Helper_Order::create_order( $customer_2->get_id() ); + WC_Helper_Order::create_order( $customer_2->get_id() ); + + $sut = new WC_Customer_Data_Store(); + add_filter( 'wc_order_statuses', array( $this, 'get_pending_only_as_order_statuses' ), 10, 0 ); + $actual_order = $sut->get_last_order( $customer_1 ); + remove_filter( 'wc_order_statuses', array( $this, 'get_pending_only_as_order_statuses' ), 10 ); + + $this->assertEquals( $last_valid_order_of_1->get_id(), $actual_order->get_id() ); + } + + /** + * @testdox 'get_last_order' works when the custom orders table is used for storing orders. + */ + public function test_get_last_customer_order_using_cot() { + global $wpdb; + + $customer_1 = WC_Helper_Customer::create_customer( 'test1', 'pass1', 'test1@example.com' ); + $customer_2 = WC_Helper_Customer::create_customer( 'test2', 'pass2', 'test2@example.com' ); + $last_valid_order = WC_Helper_Order::create_order( $customer_1->get_id() ); + + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' ); + + $sql = + 'INSERT INTO ' . OrdersTableDataStore::get_orders_table_name() . " + ( id, customer_id, status ) + VALUES + ( 1, %d, 'wc-completed' ), ( %d, %d, 'wc-completed' ), ( 3, %d, 'wc-invalid-status' ), + ( 4, %d, 'wc-completed' ), ( 5, %d, 'wc-completed' )"; + + $customer_1_id = $customer_1->get_id(); + $customer_2_id = $customer_2->get_id(); + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $query = $wpdb->prepare( $sql, $customer_1_id, $last_valid_order->get_id(), $customer_1_id, $customer_1_id, $customer_2_id, $customer_2_id ); + $wpdb->query( $query ); + //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + $sut = new WC_Customer_Data_Store(); + $actual_order = $sut->get_last_order( $customer_1 ); + + $this->assertEquals( $last_valid_order->get_id(), $actual_order->get_id() ); + } + + /** + * @testdox 'get_order_count' works when the posts table is used for storing orders. + */ + public function test_order_count_not_using_cot() { + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + + $customer_1 = WC_Helper_Customer::create_customer( 'test1', 'pass1', 'test1@example.com' ); + $customer_2 = WC_Helper_Customer::create_customer( 'test2', 'pass2', 'test2@example.com' ); + WC_Helper_Order::create_order( $customer_1->get_id() ); + WC_Helper_Order::create_order( $customer_1->get_id() ); + WC_Helper_Order::create_order( $customer_1->get_id() ); + WC_Helper_Order::create_order( $customer_1->get_id(), null, array( 'status' => 'completed' ) ); + WC_Helper_Order::create_order( $customer_2->get_id() ); + WC_Helper_Order::create_order( $customer_2->get_id() ); + + $sut = new WC_Customer_Data_Store(); + add_filter( 'wc_order_statuses', array( $this, 'get_pending_only_as_order_statuses' ), 10, 0 ); + $actual_count = $sut->get_order_count( $customer_1 ); + remove_filter( 'wc_order_statuses', array( $this, 'get_pending_only_as_order_statuses' ), 10 ); + + $this->assertEquals( 3, $actual_count ); + } + + /** + * @testdox 'get_order_count' works when the custom orders table is used for storing orders. + */ + public function test_get_order_count_using_cot() { + global $wpdb; + + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' ); + + $customer_1 = WC_Helper_Customer::create_customer( 'test1', 'pass1', 'test1@example.com' ); + $customer_2 = WC_Helper_Customer::create_customer( 'test2', 'pass2', 'test2@example.com' ); + + $sql = + 'INSERT INTO ' . OrdersTableDataStore::get_orders_table_name() . " + ( id, customer_id, status ) + VALUES + ( 1, %d, 'wc-completed' ), ( 2, %d, 'wc-completed' ), ( 3, %d, 'wc-completed' ), ( 4, %d, 'wc-invalid-status' ), + ( 5, %d, 'wc-completed' ), ( 6, %d, 'wc-completed' )"; + + $customer_1_id = $customer_1->get_id(); + $customer_2_id = $customer_2->get_id(); + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $query = $wpdb->prepare( $sql, $customer_1_id, $customer_1_id, $customer_1_id, $customer_1_id, $customer_2_id, $customer_2_id ); + $wpdb->query( $query ); + //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + $sut = new WC_Customer_Data_Store(); + $actual_count = $sut->get_order_count( $customer_1 ); + + $this->assertEquals( 3, $actual_count ); + } + + /** + * @testdox 'get_total_spent' works when the posts table is used for storing orders. + */ + public function test_get_total_spent_not_using_cot() { + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + + $customer_1 = WC_Helper_Customer::create_customer( 'test1', 'pass1', 'test1@example.com' ); + $customer_2 = WC_Helper_Customer::create_customer( 'test2', 'pass2', 'test2@example.com' ); + WC_Helper_Order::create_order( $customer_1->get_id(), null, array( 'status' => 'completed' ) ); + WC_Helper_Order::create_order( $customer_1->get_id(), null, array( 'status' => 'completed' ) ); + WC_Helper_Order::create_order( $customer_1->get_id(), null, array( 'status' => 'completed' ) ); + WC_Helper_Order::create_order( $customer_1->get_id(), null, array( 'status' => 'pending' ) ); + WC_Helper_Order::create_order( $customer_2->get_id() ); + WC_Helper_Order::create_order( $customer_2->get_id() ); + + $sut = new WC_Customer_Data_Store(); + add_filter( 'wc_order_statuses', array( $this, 'get_pending_only_as_order_statuses' ), 10, 0 ); + $actual_amount = $sut->get_total_spent( $customer_1 ); + remove_filter( 'wc_order_statuses', array( $this, 'get_pending_only_as_order_statuses' ), 10 ); + + // Each order created by WC_Helper_Order::create_order has a total amount of 50. + $this->assertEquals( '150.00', $actual_amount ); + } + + /** + * @testdox 'get_total_spent' works when the custom orders table is used for storing orders. + */ + public function test_get_total_spent_using_cot() { + global $wpdb; + + update_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' ); + + $customer_1 = WC_Helper_Customer::create_customer( 'test1', 'pass1', 'test1@example.com' ); + $customer_2 = WC_Helper_Customer::create_customer( 'test2', 'pass2', 'test2@example.com' ); + + $sql = + 'INSERT INTO ' . OrdersTableDataStore::get_orders_table_name() . " + ( id, customer_id, status, total_amount ) + VALUES + ( 1, %d, 'wc-completed', 10 ), ( 2, %d, 'wc-completed', 20 ), ( 3, %d, 'wc-completed', 30 ), ( 4, %d, 'wc-invalid-status', 40 ), + ( 5, %d, 'wc-completed', 200 ), ( 6, %d, 'wc-completed', 300 )"; + + $customer_1_id = $customer_1->get_id(); + $customer_2_id = $customer_2->get_id(); + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $query = $wpdb->prepare( $sql, $customer_1_id, $customer_1_id, $customer_1_id, $customer_1_id, $customer_2_id, $customer_2_id ); + $wpdb->query( $query ); + //phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + $sut = new WC_Customer_Data_Store(); + $actual_spent = $sut->get_total_spent( $customer_1 ); + + $this->assertEquals( '60.00', $actual_spent ); + } }