From a5cf136037662084f757682a7278b7d84e919a6b Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Tue, 18 Dec 2018 17:18:15 -0700 Subject: [PATCH 01/33] Add customer report lookup table creation to initialization query. --- .../includes/class-wc-admin-api-init.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index aeacd0c9f6b..3f96c19c5f9 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -467,6 +467,25 @@ class WC_Admin_Api_Init { PRIMARY KEY (action_id), KEY note_id (note_id) ) $collate; + CREATE TABLE {$wpdb->prefix}wc_customer_lookup ( + customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id BIGINT UNSIGNED DEFAULT NULL, + first_name varchar(255) NOT NULL, + last_name varchar(255) NOT NULL, + email varchar(100) NOT NULL, + order_count BIGINT UNSIGNED NOT NULL, + total_spend double DEFAULT 0 NOT NULL, + average_order double DEFAULT 0 NOT NULL, + last_active timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, + last_order timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_registered timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, + country char(2) DEFAULT '' NOT NULL, + postal_code varchar(20) DEFAULT '' NOT NULL, + city varchar(100) DEFAULT '' NOT NULL, + PRIMARY KEY (customer_id), + KEY user_id (user_id), + KEY email (email) + ) $collate; "; return $tables; From 91319329107c9187faebb9a9f807dc2f3c1b580e Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Tue, 18 Dec 2018 19:11:19 -0700 Subject: [PATCH 02/33] First pass at initializing the customer lookup table with existing registered customer data. --- .../includes/class-wc-admin-api-init.php | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 3f96c19c5f9..d84e25e9290 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -262,6 +262,7 @@ class WC_Admin_Api_Init { public static function regenerate_report_data() { WC_Admin_Reports_Orders_Data_Store::queue_order_stats_repopulate_database(); self::order_product_lookup_store_init(); + self::customer_lookup_store_init(); } /** @@ -330,6 +331,89 @@ class WC_Admin_Api_Init { return true; } + /** + * Init customer lookup store. + * + * @param WC_Background_Updater|null $updater Updater instance. + * @return bool + */ + public static function customer_lookup_store_init( $updater = null ) { + // TODO: this needs to be updated a bit, as it no longer runs as a part of WC_Install, there is no bg updater. + global $wpdb; + + // Backfill customer lookup table with registered customers. + $customer_ids = get_transient( 'wc_update_350_all_customers' ); + + if ( false === $customer_ids ) { + $customer_query = new WP_User_Query( + array( + 'fields' => 'ID', + 'role' => 'customer', + 'number' => -1, + ) + ); + + $customer_ids = $customer_query->get_results(); + + set_transient( 'wc_update_350_all_customers', $customer_ids, DAY_IN_SECONDS ); + } + + // Process customers until close to running out of memory timeouts on large sites then requeue. + foreach ( $customer_ids as $customer_id ) { + $customer = new WC_Customer( $customer_id ); + $order_count = $customer->get_order_count( 'edit' ); + $total_spend = $customer->get_total_spent( 'edit' ); + $last_order = $customer->get_last_order(); + $last_active = $customer->get_meta( 'wc_last_active', true, 'edit' ); + + // TODO: handle existing row in lookup table. + $wpdb->replace( + $wpdb->prefix . 'wc_customer_lookup', + array( + 'user_id' => $customer_id, + 'first_name' => $customer->get_first_name( 'edit' ), + 'last_name' => $customer->get_last_name( 'edit' ), + 'email' => $customer->get_email( 'edit' ), + 'city' => $customer->get_billing_city( 'edit' ), + 'postal_code' => $customer->get_billing_postcode( 'edit' ), + 'country' => $customer->get_billing_country( 'edit' ), + 'order_count' => $order_count, + 'total_spend' => $total_spend, + 'average_order' => $order_count ? ( $total_spend / $order_count ) : 0, + 'last_order' => $last_order ? date( 'Y-m-d H:i:s', $last_order->get_date_created( 'edit' )->getTimestamp() ) : '', + 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), + 'last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', + ), + array( + '%d', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%f', + '%f', + '%s', + '%s', + '%s', + ) + ); + + // Pop the customer ID from the array for updating the transient later should we near memory exhaustion. + unset( $customer_ids[ $customer_id ] ); + if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) { + // Update the transient for the next run to avoid processing the same orders again. + set_transient( 'wc_update_350_all_customers', $customer_ids, DAY_IN_SECONDS ); + return true; + } + } + + // TODO: Backfill customer lookup table with guests. + return true; + } + /** * Adds data stores. * @@ -509,6 +593,7 @@ class WC_Admin_Api_Init { // Initialize report tables. add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 ); + add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'customer_lookup_store_init' ), 20 ); } } From 281c06a5d4f60fc6ed8e8a4fa904704415c3b060 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 10:11:39 -0700 Subject: [PATCH 03/33] Add username column to customer report lookup table. --- plugins/woocommerce-admin/includes/class-wc-admin-api-init.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index d84e25e9290..88a82dc9976 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -371,6 +371,7 @@ class WC_Admin_Api_Init { $wpdb->prefix . 'wc_customer_lookup', array( 'user_id' => $customer_id, + 'username' => $customer->get_username( 'edit' ), 'first_name' => $customer->get_first_name( 'edit' ), 'last_name' => $customer->get_last_name( 'edit' ), 'email' => $customer->get_email( 'edit' ), @@ -392,6 +393,7 @@ class WC_Admin_Api_Init { '%s', '%s', '%s', + '%s', '%d', '%f', '%f', @@ -554,6 +556,7 @@ class WC_Admin_Api_Init { CREATE TABLE {$wpdb->prefix}wc_customer_lookup ( customer_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED DEFAULT NULL, + username varchar(60) DEFAULT '' NOT NULL, first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, email varchar(100) NOT NULL, From a2154daa4c5270d9afbff3831f8efc2b7eb7d222 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 10:12:04 -0700 Subject: [PATCH 04/33] Register customer report lookup table with core WC. --- plugins/woocommerce-admin/includes/class-wc-admin-api-init.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 88a82dc9976..4cb18076b97 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -462,6 +462,7 @@ class WC_Admin_Api_Init { "{$wpdb->prefix}wc_order_coupon_lookup", "{$wpdb->prefix}woocommerce_admin_notes", "{$wpdb->prefix}woocommerce_admin_note_actions", + "{$wpdb->prefix}wc_customer_lookup", ) ); } From 56beada2200da75512b2b9746605c5e4b9959f38 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 10:14:17 -0700 Subject: [PATCH 05/33] Update customers report endpoint parameters to match new lookup table filters. --- ...dmin-rest-reports-customers-controller.php | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 6165b3a97f6..4521537b18d 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -39,8 +39,8 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control */ protected function prepare_reports_query( $request ) { $args = array(); - $args['before'] = $request['before']; - $args['after'] = $request['after']; + $args['registered_before'] = $request['registered_before']; + $args['registered_after'] = $request['registered_after']; $args['page'] = $request['page']; $args['per_page'] = $request['per_page']; $args['name'] = $request['name']; @@ -209,14 +209,14 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control public function get_collection_params() { $params = array(); $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); - $params['before'] = array( - 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ), + $params['registered_before'] = array( + 'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['after'] = array( - 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ), + $params['registered_after'] = array( + 'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', @@ -317,6 +317,18 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'type' => 'array', 'validate_callback' => 'rest_validate_request_arg', ); + $params['last_order_before'] = array( + 'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['last_order_after'] = array( + 'description' => __( 'Limit response to objects with last order after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } } From b9f0d9fe5f14f08ce87917ef1f3741cbd8d6ae78 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 10:14:55 -0700 Subject: [PATCH 06/33] Update customers report endpoint item schema to match (visual) table designs. --- ...dmin-rest-reports-customers-controller.php | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 4521537b18d..135c9a421be 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -154,18 +154,60 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'title' => 'report_customers', 'type' => 'object', 'properties' => array( - 'id' => array( - 'description' => __( 'ID.', 'wc-admin' ), + 'customer_id' => array( + 'description' => __( 'Customer ID.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'customer_id' => array( - 'description' => __( 'Customer ID.', 'wc-admin' ), + 'user_id' => array( + 'description' => __( 'User ID.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), + 'name' => array( + 'description' => __( 'Name.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Username.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'city' => array( + 'description' => __( 'City.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_registered' => array( + 'description' => __( 'Date registered.', 'wc-admin' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_registered_gmt' => array( + 'description' => __( 'Date registered GMT.', 'wc-admin' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), 'date_last_active' => array( 'description' => __( 'Date last active.', 'wc-admin' ), 'type' => 'date-time', From 9670719fc69de9a9d8d918160f24033663bcb9ed Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 12:12:20 -0700 Subject: [PATCH 07/33] Add customers report query endpoint implementation. --- ...dmin-rest-reports-customers-controller.php | 2 +- ...class-wc-admin-reports-customers-query.php | 48 +++ ...-wc-admin-reports-customers-data-store.php | 296 ++++++++++++++++++ 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php create mode 100644 plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 135c9a421be..2cd21f8c163 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -69,7 +69,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control */ public function get_items( $request ) { $query_args = $this->prepare_reports_query( $request ); - $customers_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class. + $customers_query = new WC_Reports_Customers_Query( $query_args ); // @todo change to correct class. $report_data = $customers_query->get_data(); $out_data = array( 'totals' => get_object_vars( $report_data->totals ), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php new file mode 100644 index 00000000000..4dbdffc5c3b --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php @@ -0,0 +1,48 @@ + '2018-07-19 00:00:00', + * 'registered_after' => '2018-07-05 00:00:00', + * 'page' => 2, + * 'avg_order_value_min' => 100, + * 'country' => 'GB', + * ); + * $report = new WC_Admin_Reports_Customers_Query( $args ); + * $mydata = $report->get_data(); + * + * @package WooCommerce Admin/Classes + */ + +defined( 'ABSPATH' ) || exit; + +/** + * WC_Admin_Reports_Customers_Query + */ +class WC_Admin_Reports_Customers_Query extends WC_Admin_Reports_Query { + + /** + * Valid fields for Customers report. + * + * @return array + */ + protected function get_default_query_vars() { + return array(); + } + + /** + * Get product data based on the current query vars. + * + * @return array + */ + public function get_data() { + $args = apply_filters( 'woocommerce_reports_customers_query_args', $this->get_query_vars() ); + + $data_store = WC_Data_Store::load( 'report-customers' ); + $results = $data_store->get_data( $args ); + return apply_filters( 'woocommerce_reports_customers_select_query', $results, $args ); + } + +} diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php new file mode 100644 index 00000000000..80053cf4942 --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -0,0 +1,296 @@ + 'intval', + 'user_id' => 'intval', + 'date_registered' => 'strval', + 'date_last_active' => 'strval', + 'orders_count' => 'intval', + 'total_spend' => 'floatval', + 'avg_order_value' => 'floatval', + ); + + /** + * SQL columns to select in the db query and their mapping to SQL code. + * + * @var array + */ + protected $report_columns = array( + 'customer_id' => 'customer_id', + 'user_id' => 'user_id', + 'username' => 'username', + 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // TODO: what does this mean for RTL? + 'email' => 'email', + 'country' => 'country', + 'city' => 'city', + 'postcode' => 'postcode', + 'date_registered' => 'date_registered', + 'date_last_active' => 'date_last_active', + 'orders_count' => 'orders_count', + 'total_spend' => 'total_spend', + 'avg_order_value' => 'avg_order_value', + ); + + /** + * Maps ordering specified by the user to columns in the database/fields in the data. + * + * @param string $order_by Sorting criterion. + * @return string + */ + protected function normalize_order_by( $order_by ) { + if ( 'name' === $order_by ) { + return "CONCAT_WS( ' ', first_name, last_name )"; + } + + return $order_by; + } + + /** + * Fills ORDER BY clause of SQL request based on user supplied parameters. + * + * @param array $query_args Parameters supplied by the user. + * @return array + */ + protected function get_order_by_sql_params( $query_args ) { + $sql_query['order_by_clause'] = ''; + + if ( isset( $query_args['orderby'] ) ) { + $sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] ); + } + + if ( isset( $query_args['order'] ) ) { + $sql_query['order_by_clause'] .= ' ' . $query_args['order']; + } else { + $sql_query['order_by_clause'] .= ' DESC'; + } + + return $sql_query; + } + + /** + * Fills WHERE clause of SQL request with date-related constraints. + * + * @param array $query_args Parameters supplied by the user. + * @param string $table_name Name of the db table relevant for the date constraint. + * @return array + */ + protected function get_time_period_sql_params( $query_args, $table_name ) { + $sql_query = array( + 'from_clause' => '', + 'where_time_clause' => '', + 'where_clause' => '', + ); + $date_param_mapping = array( + 'registered' => 'date_registered', + 'last_active' => 'date_last_active', + 'last_order' => 'date_last_order', + ); + + // TODO: handle any/or match? + foreach ( $date_param_mapping as $query_param => $column_name ) { + $before_arg = $query_param . '_before'; + $after_arg = $query_param . '_after'; + + if ( ! empty( $query_args[ $before_arg ] ) ) { + $datetime = new DateTime( $query_args[ $before_arg ] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.{$column_name} <= '$datetime_str'"; + } + + if ( ! empty( $query_args[ $after_arg ] ) ) { + $datetime = new DateTime( $query_args[ $after_arg ] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $sql_query['where_time_clause'] .= " AND {$table_name}.{$column_name} >= '$datetime_str'"; + } + } + + return $sql_query; + } + + /** + * Updates the database query with parameters used for Customers report: categories and order status. + * + * @param array $query_args Query arguments supplied by the user. + * @return array Array of parameters used for SQL query. + */ + protected function get_sql_query_params( $query_args ) { + global $wpdb; + $order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME; + + $sql_query_params = $this->get_time_period_sql_params( $query_args, $order_product_lookup_table ); + $sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) ); + $sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) ); + + $match_operator = $this->get_match_operator( $query_args ); + $where_clauses = array(); + + $exact_match_params = array( + 'name', + 'username', + 'email', + 'country', + ); + + foreach ( $exact_match_params as $exact_match_param ) { + if ( ! empty( $query_args[ $exact_match_param ] ) ) { + $where_clauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$exact_match_param} = %s", $query_args[ $exact_match_param ] ); + } + } + + $numeric_params = array( + 'orders_count' => '%d', + 'total_spend' => '%f', + 'avg_order_value' => '%f', + ); + + foreach ( $numeric_params as $numeric_param => $sql_format ) { + $subclauses = array(); + $min_param = $numeric_param . '_min'; + $max_param = $numeric_param . '_max'; + + if ( isset( $query_args[ $min_param ] ) ) { + $subclauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$numeric_param} >= {$sql_format}", $query_args[ $min_param ] ); + } + + if ( isset( $query_args[ $max_param ] ) ) { + $subclauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$numeric_param} <= {$sql_format}", $query_args[ $max_param ] ); + } + + if ( $subclauses ) { + $where_clauses[] = '(' . implode( " {$match_operator} ", $subclauses ) . ')'; + } + } + + if ( $where_clauses ) { + $sql_query_params['where_clause'] = implode( " {$match_operator} ", $where_clauses ); + } + + return $sql_query_params; + } + + /** + * Returns the report data based on parameters supplied by the user. + * + * @param array $query_args Query parameters. + * @return stdClass|WP_Error Data. + */ + public function get_data( $query_args ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + + // These defaults are only partially applied when used via REST API, as that has its own defaults. + $defaults = array( + 'per_page' => get_option( 'posts_per_page' ), + 'page' => 1, + 'order' => 'DESC', + 'orderby' => 'date_registered', + 'fields' => '*', + ); + $query_args = wp_parse_args( $query_args, $defaults ); + + $cache_key = $this->get_cache_key( $query_args ); + $data = wp_cache_get( $cache_key, $this->cache_group ); + + if ( false === $data ) { + $data = (object) array( + 'data' => array(), + 'total' => 0, + 'pages' => 0, + 'page_no' => 0, + ); + + $selections = $this->selected_columns( $query_args ); + $sql_query_params = $this->get_sql_query_params( $query_args ); + + $db_records_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM ( + SELECT + customer_id + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + customer_id + ) AS tt" + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] ); + if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { + return $data; + } + + $customer_data = $wpdb->get_results( + "SELECT + {$selections} + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + GROUP BY + customer_id + ORDER BY + {$sql_query_params['order_by_clause']} + {$sql_query_params['limit']} + ", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + if ( null === $customer_data ) { + return $data; + } + + $customer_data = array_map( array( $this, 'cast_numbers' ), $customer_data ); + $data = (object) array( + 'data' => $customer_data, + 'total' => $db_records_count, + 'pages' => $total_pages, + 'page_no' => (int) $query_args['page'], + ); + + wp_cache_set( $cache_key, $data, $this->cache_group ); + } + + return $data; + } + + /** + * Returns string to be used as cache key for the data. + * + * @param array $params Query parameters. + * @return string + */ + protected function get_cache_key( $params ) { + return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); + } + +} From 746120e254e7869124955f474cd66f0b8752ff72 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 18:14:32 -0700 Subject: [PATCH 08/33] Change customer report lookup table columns to match API parameters. --- .../includes/class-wc-admin-api-init.php | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 4cb18076b97..eba1568459c 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -370,20 +370,20 @@ class WC_Admin_Api_Init { $wpdb->replace( $wpdb->prefix . 'wc_customer_lookup', array( - 'user_id' => $customer_id, - 'username' => $customer->get_username( 'edit' ), - 'first_name' => $customer->get_first_name( 'edit' ), - 'last_name' => $customer->get_last_name( 'edit' ), - 'email' => $customer->get_email( 'edit' ), - 'city' => $customer->get_billing_city( 'edit' ), - 'postal_code' => $customer->get_billing_postcode( 'edit' ), - 'country' => $customer->get_billing_country( 'edit' ), - 'order_count' => $order_count, - 'total_spend' => $total_spend, - 'average_order' => $order_count ? ( $total_spend / $order_count ) : 0, - 'last_order' => $last_order ? date( 'Y-m-d H:i:s', $last_order->get_date_created( 'edit' )->getTimestamp() ) : '', - 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), - 'last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', + 'user_id' => $customer_id, + 'username' => $customer->get_username( 'edit' ), + 'first_name' => $customer->get_first_name( 'edit' ), + 'last_name' => $customer->get_last_name( 'edit' ), + 'email' => $customer->get_email( 'edit' ), + 'city' => $customer->get_billing_city( 'edit' ), + 'postcode' => $customer->get_billing_postcode( 'edit' ), + 'country' => $customer->get_billing_country( 'edit' ), + 'orders_count' => $order_count, + 'total_spend' => $total_spend, + 'avg_order_value' => $order_count ? ( $total_spend / $order_count ) : 0, + 'date_last_order' => $last_order ? date( 'Y-m-d H:i:s', $last_order->get_date_created( 'edit' )->getTimestamp() ) : '', + 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), + 'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', ), array( '%d', @@ -561,14 +561,14 @@ class WC_Admin_Api_Init { first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, email varchar(100) NOT NULL, - order_count BIGINT UNSIGNED NOT NULL, + orders_count BIGINT UNSIGNED NOT NULL, total_spend double DEFAULT 0 NOT NULL, - average_order double DEFAULT 0 NOT NULL, - last_active timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, - last_order timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, + avg_order_value double DEFAULT 0 NOT NULL, + date_last_active timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_last_order timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, date_registered timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, country char(2) DEFAULT '' NOT NULL, - postal_code varchar(20) DEFAULT '' NOT NULL, + postcode varchar(20) DEFAULT '' NOT NULL, city varchar(100) DEFAULT '' NOT NULL, PRIMARY KEY (customer_id), KEY user_id (user_id), From 6e5fef2f7b372aa2fbd072317756c1b20472e8d9 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 18:19:12 -0700 Subject: [PATCH 09/33] Include customers report files. --- .../woocommerce-admin/includes/class-wc-admin-api-init.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index eba1568459c..e5cd100ffa6 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -55,6 +55,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-stats-query.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-query.php'; // Data stores. require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php'; @@ -69,6 +70,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-coupons-stats-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-stats-data-store.php'; + require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-data-store.php'; // Data triggers. require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php'; @@ -130,6 +132,7 @@ class WC_Admin_Api_Init { 'WC_Admin_REST_Reports_Stock_Controller', 'WC_Admin_REST_Reports_Downloads_Controller', 'WC_Admin_REST_Reports_Downloads_Stats_Controller', + 'WC_Admin_REST_Reports_Customers_Controller', ); foreach ( $controllers as $controller ) { @@ -439,6 +442,7 @@ class WC_Admin_Api_Init { 'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store', 'report-downloads-stats' => 'WC_Admin_Reports_Downloads_Stats_Data_Store', 'admin-note' => 'WC_Admin_Notes_Data_Store', + 'report-customers' => 'WC_Admin_Reports_Customers_Data_Store', ) ); } From 0c919add5db5acd44cf363ee4ce3a384fda40675 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 18:20:27 -0700 Subject: [PATCH 10/33] Fix query params in customers report controller. --- ...dmin-rest-reports-customers-controller.php | 66 +++++++++++++++++-- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 2cd21f8c163..9baae0ff868 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -43,21 +43,26 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['registered_after'] = $request['registered_after']; $args['page'] = $request['page']; $args['per_page'] = $request['per_page']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['match'] = $request['match']; $args['name'] = $request['name']; $args['username'] = $request['username']; $args['email'] = $request['email']; $args['country'] = $request['country']; $args['last_active_before'] = $request['last_active_before']; $args['last_active_after'] = $request['last_active_after']; - $args['order_count_min'] = $request['order_count_min']; - $args['order_count_max'] = $request['order_count_max']; - $args['order_count_between'] = $request['order_count_between']; + $args['orders_count_min'] = $request['orders_count_min']; + $args['orders_count_max'] = $request['orders_count_max']; + $args['orders_count_between'] = $request['orders_count_between']; $args['total_spend_min'] = $request['total_spend_min']; $args['total_spend_max'] = $request['total_spend_max']; $args['total_spend_between'] = $request['total_spend_between']; $args['avg_order_value_min'] = $request['avg_order_value_min']; $args['avg_order_value_max'] = $request['avg_order_value_max']; $args['avg_order_value_between'] = $request['avg_order_value_between']; + $args['last_order_before'] = $request['last_order_before']; + $args['last_order_after'] = $request['last_order_after']; return $args; } @@ -280,7 +285,42 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['name'] = array( + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'wc-admin' ), + 'type' => 'string', + 'default' => 'date_registered', + 'enum' => array( + 'username', + 'name', + 'country', + 'city', + 'postcode', + 'date_registered', + 'date_last_active', + 'orders_count', + 'total_spend', + 'avg_order_value', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['match'] = array( + 'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'wc-admin' ), + 'type' => 'string', + 'default' => 'all', + 'enum' => array( + 'all', + 'any', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['name'] = array( 'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', @@ -312,19 +352,31 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['order_count_min'] = array( + $params['registered_before'] = array( + 'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['registered_after'] = array( + 'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orders_count_min'] = array( 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['order_count_max'] = array( + $params['orders_count_max'] = array( 'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['order_count_between'] = array( + $params['orders_count_between'] = array( 'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ), 'type' => 'array', 'validate_callback' => 'rest_validate_request_arg', From a180566c2f5047b43e436c604491cf1c1ce8d8b7 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 18:20:57 -0700 Subject: [PATCH 11/33] Set default parameters for customers report query. --- .../includes/class-wc-admin-reports-customers-query.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php index 4dbdffc5c3b..937a0bf7b42 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-query.php @@ -29,7 +29,13 @@ class WC_Admin_Reports_Customers_Query extends WC_Admin_Reports_Query { * @return array */ protected function get_default_query_vars() { - return array(); + return array( + 'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default. + 'page' => 1, + 'order' => 'DESC', + 'orderby' => 'date_registered', + 'fields' => '*', + ); } /** From 97e6f795e947c4d270c842a4078ae37e260ee47e Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Wed, 19 Dec 2018 18:22:39 -0700 Subject: [PATCH 12/33] Fix copy-pasta in customers report REST controller. --- ...dmin-rest-reports-customers-controller.php | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 9baae0ff868..d52d226fe14 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -74,19 +74,20 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control */ public function get_items( $request ) { $query_args = $this->prepare_reports_query( $request ); - $customers_query = new WC_Reports_Customers_Query( $query_args ); // @todo change to correct class. + $customers_query = new WC_Admin_Reports_Customers_Query( $query_args ); $report_data = $customers_query->get_data(); - $out_data = array( - 'totals' => get_object_vars( $report_data->totals ), - 'customers' => array(), - ); - foreach ( $report_data->customers as $customer_data ) { - $item_data = $this->prepare_item_for_response( (object) $customer_data, $request ); - $out_data['customers'][] = $item_data; + + $data = array(); + + foreach ( $report_data->data as $customer_data ) { + $item = $this->prepare_item_for_response( $customer_data, $request ); + $data[] = $this->prepare_response_for_collection( $item ); } - $response = rest_ensure_response( $out_data ); + + $response = rest_ensure_response( $data ); $response->header( 'X-WP-Total', (int) $report_data->total ); $response->header( 'X-WP-TotalPages', (int) $report_data->pages ); + $page = $report_data->page_no; $max_pages = $report_data->pages; $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); @@ -103,20 +104,20 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $next_link = add_query_arg( 'page', $next_page, $base ); $response->link_header( 'next', $next_link ); } + return $response; } /** * Prepare a report object for serialization. * - * @param stdClass $report Report data. + * @param array $report Report data. * @param WP_REST_Request $request Request object. * @return WP_REST_Response */ public function prepare_item_for_response( $report, $request ) { - $data = get_object_vars( $report ); $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->add_additional_fields_to_object( $report, $request ); $data = $this->filter_response_by_context( $data, $context ); // Wrap the data in a response object. $response = rest_ensure_response( $data ); @@ -136,14 +137,16 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control /** * Prepare links for the request. * - * @param WC_Reports_Query $object Object data. + * @param array $object Object data. * @return array */ protected function prepare_links( $object ) { $links = array( 'customer' => array( - 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->customer_id ) ), + // TODO: is this meant to be a core WC link? + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object['customer_id'] ) ), ), + // TODO: add user link? ); return $links; } From a75a33c32f086d7e49b4023f11d7b2c898fc8673 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 09:30:33 -0700 Subject: [PATCH 13/33] Handle match parameter in customers report data store. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note: before/after and min/max ranges are always ‘AND’. --- ...-wc-admin-reports-customers-data-store.php | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 80053cf4942..f0ce74d3fcb 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -99,33 +99,43 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store * @return array */ protected function get_time_period_sql_params( $query_args, $table_name ) { - $sql_query = array( + $sql_query = array( 'from_clause' => '', 'where_time_clause' => '', 'where_clause' => '', ); - $date_param_mapping = array( + $date_param_mapping = array( 'registered' => 'date_registered', 'last_active' => 'date_last_active', 'last_order' => 'date_last_order', ); + $match_operator = $this->get_match_operator( $query_args ); + $where_time_clauses = array(); - // TODO: handle any/or match? foreach ( $date_param_mapping as $query_param => $column_name ) { + $subclauses = array(); $before_arg = $query_param . '_before'; $after_arg = $query_param . '_after'; if ( ! empty( $query_args[ $before_arg ] ) ) { - $datetime = new DateTime( $query_args[ $before_arg ] ); - $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $sql_query['where_time_clause'] .= " AND {$table_name}.{$column_name} <= '$datetime_str'"; + $datetime = new DateTime( $query_args[ $before_arg ] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $subclauses[] = "{$table_name}.{$column_name} <= '$datetime_str'"; } if ( ! empty( $query_args[ $after_arg ] ) ) { - $datetime = new DateTime( $query_args[ $after_arg ] ); - $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $sql_query['where_time_clause'] .= " AND {$table_name}.{$column_name} >= '$datetime_str'"; + $datetime = new DateTime( $query_args[ $after_arg ] ); + $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); + $subclauses[] = "{$table_name}.{$column_name} <= '$datetime_str'"; } + + if ( $subclauses ) { + $where_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')'; + } + } + + if ( $where_time_clauses ) { + $sql_query['where_time_clause'] = implode( " {$match_operator} ", $where_time_clauses ); } return $sql_query; @@ -157,7 +167,10 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store foreach ( $exact_match_params as $exact_match_param ) { if ( ! empty( $query_args[ $exact_match_param ] ) ) { - $where_clauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$exact_match_param} = %s", $query_args[ $exact_match_param ] ); + $where_clauses[] = $wpdb->prepare( + "{$order_product_lookup_table}.{$exact_match_param} = %s", + $query_args[ $exact_match_param ] + ); // WPCS: unprepared SQL ok. } } @@ -173,20 +186,27 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $max_param = $numeric_param . '_max'; if ( isset( $query_args[ $min_param ] ) ) { - $subclauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$numeric_param} >= {$sql_format}", $query_args[ $min_param ] ); + $subclauses[] = $wpdb->prepare( + "{$order_product_lookup_table}.{$numeric_param} >= {$sql_format}", + $query_args[ $min_param ] + ); // WPCS: unprepared SQL ok. } if ( isset( $query_args[ $max_param ] ) ) { - $subclauses[] = $wpdb->prepare( "{$order_product_lookup_table}.{$numeric_param} <= {$sql_format}", $query_args[ $max_param ] ); + $subclauses[] = $wpdb->prepare( + "{$order_product_lookup_table}.{$numeric_param} <= {$sql_format}", + $query_args[ $max_param ] + ); // WPCS: unprepared SQL ok. } if ( $subclauses ) { - $where_clauses[] = '(' . implode( " {$match_operator} ", $subclauses ) . ')'; + $where_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')'; } } if ( $where_clauses ) { - $sql_query_params['where_clause'] = implode( " {$match_operator} ", $where_clauses ); + $preceding_match = empty( $sql_query_params['where_time_clause'] ) ? '' : " {$match_operator} "; + $sql_query_params['where_clause'] = $preceding_match . implode( " {$match_operator} ", $where_clauses ); } return $sql_query_params; From fc20d2bb1b1ef7b96c548d21ff98e81171331e54 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 09:31:18 -0700 Subject: [PATCH 14/33] Remove unnecessary subquery from customer reports data store records count query. --- ...-wc-admin-reports-customers-data-store.php | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index f0ce74d3fcb..2649adaca6a 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -248,18 +248,14 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $sql_query_params = $this->get_sql_query_params( $query_args ); $db_records_count = (int) $wpdb->get_var( - "SELECT COUNT(*) FROM ( - SELECT - customer_id - FROM - {$table_name} - {$sql_query_params['from_clause']} - WHERE - {$sql_query_params['where_time_clause']} - {$sql_query_params['where_clause']} - GROUP BY - customer_id - ) AS tt" + "SELECT COUNT(*) + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + " ); // WPCS: cache ok, DB call ok, unprepared SQL ok. $total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] ); From dd87522a5ea58079242695dfefc33f20603c2ddf Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 09:46:29 -0700 Subject: [PATCH 15/33] =?UTF-8?q?Customers=20report=20endpoint:=20only=20i?= =?UTF-8?q?nclude=20customer=20link=20if=20they=E2=80=99re=20a=20registere?= =?UTF-8?q?d=20user.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ass-wc-admin-rest-reports-customers-controller.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index d52d226fe14..52f80ee2196 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -141,14 +141,15 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control * @return array */ protected function prepare_links( $object ) { - $links = array( + if ( empty( $object['user_id'] ) ) { + return array(); + } + + return array( 'customer' => array( - // TODO: is this meant to be a core WC link? - 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object['customer_id'] ) ), + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object['user_id'] ) ), ), - // TODO: add user link? ); - return $links; } /** From d0e2c5162fd79f8ae539585ff8205b36c12496d1 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 11:09:39 -0700 Subject: [PATCH 16/33] Place a UNIQUE constraint on the user_id key for the customer report lookup table. --- plugins/woocommerce-admin/includes/class-wc-admin-api-init.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index e5cd100ffa6..e560023621d 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -575,7 +575,7 @@ class WC_Admin_Api_Init { postcode varchar(20) DEFAULT '' NOT NULL, city varchar(100) DEFAULT '' NOT NULL, PRIMARY KEY (customer_id), - KEY user_id (user_id), + UNIQUE KEY user_id (user_id), KEY email (email) ) $collate; "; From 2ea61e9a863ab577a2b7012f7d6a8215263c3fea Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 11:11:24 -0700 Subject: [PATCH 17/33] Move registered customer lookup update logic into data store class. --- .../includes/class-wc-admin-api-init.php | 48 ++--------------- ...-wc-admin-reports-customers-data-store.php | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index e560023621d..99e5c787d0a 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -363,51 +363,13 @@ class WC_Admin_Api_Init { // Process customers until close to running out of memory timeouts on large sites then requeue. foreach ( $customer_ids as $customer_id ) { - $customer = new WC_Customer( $customer_id ); - $order_count = $customer->get_order_count( 'edit' ); - $total_spend = $customer->get_total_spent( 'edit' ); - $last_order = $customer->get_last_order(); - $last_active = $customer->get_meta( 'wc_last_active', true, 'edit' ); + $result = WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id ); - // TODO: handle existing row in lookup table. - $wpdb->replace( - $wpdb->prefix . 'wc_customer_lookup', - array( - 'user_id' => $customer_id, - 'username' => $customer->get_username( 'edit' ), - 'first_name' => $customer->get_first_name( 'edit' ), - 'last_name' => $customer->get_last_name( 'edit' ), - 'email' => $customer->get_email( 'edit' ), - 'city' => $customer->get_billing_city( 'edit' ), - 'postcode' => $customer->get_billing_postcode( 'edit' ), - 'country' => $customer->get_billing_country( 'edit' ), - 'orders_count' => $order_count, - 'total_spend' => $total_spend, - 'avg_order_value' => $order_count ? ( $total_spend / $order_count ) : 0, - 'date_last_order' => $last_order ? date( 'Y-m-d H:i:s', $last_order->get_date_created( 'edit' )->getTimestamp() ) : '', - 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), - 'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', - ), - array( - '%d', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%d', - '%f', - '%f', - '%s', - '%s', - '%s', - ) - ); + if ( $result ) { + // Pop the customer ID from the array for updating the transient later should we near memory exhaustion. + unset( $customer_ids[ $customer_id ] ); + } - // Pop the customer ID from the array for updating the transient later should we near memory exhaustion. - unset( $customer_ids[ $customer_id ] ); if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) { // Update the transient for the next run to avoid processing the same orders again. set_transient( 'wc_update_350_all_customers', $customer_ids, DAY_IN_SECONDS ); diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 2649adaca6a..0142ed2a1f1 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -299,6 +299,59 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store return $data; } + /** + * Update the database with customer data. + * + * @param int $user_id WP User ID to update customer data for. + * @return int|bool|null Number or rows modified or false on failure. + */ + public static function update_registered_customer( $user_id ) { + global $wpdb; + + $customer = new WC_Customer( $user_id ); + $order_count = $customer->get_order_count( 'edit' ); + $total_spend = $customer->get_total_spent( 'edit' ); + $last_order = $customer->get_last_order(); + $last_active = $customer->get_meta( 'wc_last_active', true, 'edit' ); + + // TODO: try to preserve customer_id for existing user_id? + return $wpdb->replace( + $wpdb->prefix . self::TABLE_NAME, + array( + 'user_id' => $user_id, + 'username' => $customer->get_username( 'edit' ), + 'first_name' => $customer->get_first_name( 'edit' ), + 'last_name' => $customer->get_last_name( 'edit' ), + 'email' => $customer->get_email( 'edit' ), + 'city' => $customer->get_billing_city( 'edit' ), + 'postcode' => $customer->get_billing_postcode( 'edit' ), + 'country' => $customer->get_billing_country( 'edit' ), + 'orders_count' => $order_count, + 'total_spend' => $total_spend, + 'avg_order_value' => $order_count ? ( $total_spend / $order_count ) : 0, + 'date_last_order' => $last_order ? date( 'Y-m-d H:i:s', $last_order->get_date_created( 'edit' )->getTimestamp() ) : '', + 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), + 'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', + ), + array( + '%d', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%f', + '%f', + '%s', + '%s', + '%s', + ) + ); + } + /** * Returns string to be used as cache key for the data. * From 1832450fd14f0489df0fe87973cba13cf3c6c2d8 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 11:40:30 -0700 Subject: [PATCH 18/33] Update customer lookup table when orders (with registered customers) are created, updated, or refunded. --- .../includes/class-wc-admin-api-init.php | 9 ++++++ ...-wc-admin-reports-customers-data-store.php | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 99e5c787d0a..6ffabdefef3 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -29,6 +29,8 @@ class WC_Admin_Api_Init { // Initialize Orders data store class's static vars. add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 ); + // Initialize Customers Report data store class's static vars. + add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ), 20 ); } /** @@ -334,6 +336,13 @@ class WC_Admin_Api_Init { return true; } + /** + * Init customers report data store. + */ + public static function customers_report_data_store_init() { + WC_Admin_Reports_Customers_Data_Store::init(); + } + /** * Init customer lookup store. * diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 0142ed2a1f1..e28ce12424e 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -55,6 +55,15 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store 'avg_order_value' => 'avg_order_value', ); + /** + * Set up all the hooks for maintaining and populating table data. + */ + public static function init() { + add_action( 'woocommerce_new_order', array( __CLASS__, 'sync_order' ) ); + add_action( 'woocommerce_update_order', array( __CLASS__, 'sync_order' ) ); + add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) ); + } + /** * Maps ordering specified by the user to columns in the database/fields in the data. * @@ -299,6 +308,29 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store return $data; } + /** + * Add customer information to the lookup table when orders are created or modified. + * + * @param int $post_id Post ID. + */ + public static function sync_order( $post_id ) { + if ( 'shop_order' !== get_post_type( $post_id ) ) { + return; + } + + $order = wc_get_order( $post_id ); + if ( ! $order ) { + return; + } + + $customer_id = $order->get_customer_id(); + if ( 0 === $customer_id ) { + return; + } + + self::update_registered_customer( $customer_id ); + } + /** * Update the database with customer data. * From 04a30e668b9c4e7aa1ed5b8645fc3c6691097ffe Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 15:31:20 -0700 Subject: [PATCH 19/33] Customer report data store: fix SQL when where clauses are empty. --- .../class-wc-admin-reports-customers-data-store.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index e28ce12424e..7f14f062cb8 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -144,7 +144,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } if ( $where_time_clauses ) { - $sql_query['where_time_clause'] = implode( " {$match_operator} ", $where_time_clauses ); + $sql_query['where_time_clause'] = ' AND ' . implode( " {$match_operator} ", $where_time_clauses ); } return $sql_query; @@ -214,7 +214,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } if ( $where_clauses ) { - $preceding_match = empty( $sql_query_params['where_time_clause'] ) ? '' : " {$match_operator} "; + $preceding_match = empty( $sql_query_params['where_time_clause'] ) ? ' AND ' : " {$match_operator} "; $sql_query_params['where_clause'] = $preceding_match . implode( " {$match_operator} ", $where_clauses ); } @@ -262,6 +262,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store {$table_name} {$sql_query_params['from_clause']} WHERE + 1=1 {$sql_query_params['where_time_clause']} {$sql_query_params['where_clause']} " @@ -279,6 +280,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store {$table_name} {$sql_query_params['from_clause']} WHERE + 1=1 {$sql_query_params['where_time_clause']} {$sql_query_params['where_clause']} GROUP BY From 1a4b8906607069ba65f606b709d66e413f6624fa Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 15:35:30 -0700 Subject: [PATCH 20/33] Add unit tests for customers report endpoint. --- .../tests/api/reports-customers.php | 145 ++++++++++++++++++ .../helpers/class-wc-helper-reports.php | 1 + 2 files changed, 146 insertions(+) create mode 100644 plugins/woocommerce-admin/tests/api/reports-customers.php diff --git a/plugins/woocommerce-admin/tests/api/reports-customers.php b/plugins/woocommerce-admin/tests/api/reports-customers.php new file mode 100644 index 00000000000..8f765a987da --- /dev/null +++ b/plugins/woocommerce-admin/tests/api/reports-customers.php @@ -0,0 +1,145 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( $this->endpoint, $routes ); + } + + /** + * Test reports schema. + * + * @since 3.5.0 + */ + public function test_reports_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', $this->endpoint ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 14, count( $properties ) ); + $this->assertArrayHasKey( 'customer_id', $properties ); + $this->assertArrayHasKey( 'user_id', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'username', $properties ); + $this->assertArrayHasKey( 'country', $properties ); + $this->assertArrayHasKey( 'city', $properties ); + $this->assertArrayHasKey( 'postcode', $properties ); + $this->assertArrayHasKey( 'date_registered', $properties ); + $this->assertArrayHasKey( 'date_registered_gmt', $properties ); + $this->assertArrayHasKey( 'date_last_active', $properties ); + $this->assertArrayHasKey( 'date_last_active_gmt', $properties ); + $this->assertArrayHasKey( 'orders_count', $properties ); + $this->assertArrayHasKey( 'total_spend', $properties ); + $this->assertArrayHasKey( 'avg_order_value', $properties ); + } + + /** + * Test getting reports without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting reports. + * + * @since 3.5.0 + */ + public function test_get_reports() { + wp_set_current_user( $this->user ); + WC_Helper_Reports::reset_stats_dbs(); + + $test_customers = array(); + + // Create 10 test customers. + for ( $i = 1; $i <= 10; $i++ ) { + $test_customers[] = WC_Helper_Customer::create_customer( "customer{$i}", 'password', "customer{$i}@example.com" ); + } + + // Initialize the report lookup table. + delete_transient( 'wc_update_350_all_customers' ); + WC_Admin_Api_Init::customer_lookup_store_init(); + + // Create a test product for use in an order. + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_regular_price( 25 ); + $product->save(); + + // Place an order for the first test customer. + $order = WC_Helper_Order::create_order( $test_customers[0]->get_id(), $product ); + $order->set_status( 'processing' ); + $order->set_total( 100 ); + $order->save(); + + $request = new WP_REST_Request( 'GET', $this->endpoint ); + $request->set_query_params( + array( + 'per_page' => 5, + 'order' => 'asc', + 'orderby' => 'username', + ) + ); + + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + $headers = $response->get_headers(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 5, count( $reports ) ); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertEquals( 10, $headers['X-WP-Total'] ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + $this->assertEquals( 2, $headers['X-WP-TotalPages'] ); + $this->assertEquals( $test_customers[0]->get_id(), $reports[0]['user_id'] ); + $this->assertEquals( 1, $reports[0]['orders_count'] ); + $this->assertEquals( 100, $reports[0]['total_spend'] ); + } +} diff --git a/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-reports.php b/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-reports.php index 11404708c24..86d93727db1 100644 --- a/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-reports.php +++ b/plugins/woocommerce-admin/tests/framework/helpers/class-wc-helper-reports.php @@ -20,5 +20,6 @@ class WC_Helper_Reports { $wpdb->query( "DELETE FROM $wpdb->prefix" . WC_Admin_Reports_Orders_Data_Store::TABLE_NAME ); // @codingStandardsIgnoreLine. $wpdb->query( "DELETE FROM $wpdb->prefix" . WC_Admin_Reports_Products_Data_Store::TABLE_NAME ); // @codingStandardsIgnoreLine. $wpdb->query( "DELETE FROM $wpdb->prefix" . WC_Admin_Reports_Coupons_Data_Store::TABLE_NAME ); // @codingStandardsIgnoreLine. + $wpdb->query( "DELETE FROM $wpdb->prefix" . WC_Admin_Reports_Customers_Data_Store::TABLE_NAME ); // @codingStandardsIgnoreLine. } } From 737331dd3b2f6cd79f20953ef40f761b5c5edb43 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 16:19:40 -0700 Subject: [PATCH 21/33] Add missing _gmt date fields to customers report response items. --- ...dmin-rest-reports-customers-controller.php | 11 +++-- .../tests/api/reports-customers.php | 40 ++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 52f80ee2196..2039437b32b 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -116,9 +116,14 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control * @return WP_REST_Response */ public function prepare_item_for_response( $report, $request ) { - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $report, $request ); - $data = $this->filter_response_by_context( $data, $context ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $report, $request ); + $data['date_registered_gmt'] = wc_rest_prepare_date_response( $data['date_registered'] ); + $data['date_registered'] = wc_rest_prepare_date_response( $data['date_registered'], false ); + $data['date_last_active_gmt'] = wc_rest_prepare_date_response( $data['date_last_active'] ); + $data['date_last_active'] = wc_rest_prepare_date_response( $data['date_last_active'], false ); + $data = $this->filter_response_by_context( $data, $context ); + // Wrap the data in a response object. $response = rest_ensure_response( $data ); $response->add_links( $this->prepare_links( $report ) ); diff --git a/plugins/woocommerce-admin/tests/api/reports-customers.php b/plugins/woocommerce-admin/tests/api/reports-customers.php index 8f765a987da..84064a9bdbf 100644 --- a/plugins/woocommerce-admin/tests/api/reports-customers.php +++ b/plugins/woocommerce-admin/tests/api/reports-customers.php @@ -46,6 +46,28 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( $this->endpoint, $routes ); } + /** + * Asserts the report item schema is correct. + * + * @param array $schema Item to check schema. + */ + public function assert_report_item_schema( $schema ) { + $this->assertArrayHasKey( 'customer_id', $schema ); + $this->assertArrayHasKey( 'user_id', $schema ); + $this->assertArrayHasKey( 'name', $schema ); + $this->assertArrayHasKey( 'username', $schema ); + $this->assertArrayHasKey( 'country', $schema ); + $this->assertArrayHasKey( 'city', $schema ); + $this->assertArrayHasKey( 'postcode', $schema ); + $this->assertArrayHasKey( 'date_registered', $schema ); + $this->assertArrayHasKey( 'date_registered_gmt', $schema ); + $this->assertArrayHasKey( 'date_last_active', $schema ); + $this->assertArrayHasKey( 'date_last_active_gmt', $schema ); + $this->assertArrayHasKey( 'orders_count', $schema ); + $this->assertArrayHasKey( 'total_spend', $schema ); + $this->assertArrayHasKey( 'avg_order_value', $schema ); + } + /** * Test reports schema. * @@ -59,21 +81,8 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertEquals( 14, count( $properties ) ); - $this->assertArrayHasKey( 'customer_id', $properties ); - $this->assertArrayHasKey( 'user_id', $properties ); - $this->assertArrayHasKey( 'name', $properties ); - $this->assertArrayHasKey( 'username', $properties ); - $this->assertArrayHasKey( 'country', $properties ); - $this->assertArrayHasKey( 'city', $properties ); - $this->assertArrayHasKey( 'postcode', $properties ); - $this->assertArrayHasKey( 'date_registered', $properties ); - $this->assertArrayHasKey( 'date_registered_gmt', $properties ); - $this->assertArrayHasKey( 'date_last_active', $properties ); - $this->assertArrayHasKey( 'date_last_active_gmt', $properties ); - $this->assertArrayHasKey( 'orders_count', $properties ); - $this->assertArrayHasKey( 'total_spend', $properties ); - $this->assertArrayHasKey( 'avg_order_value', $properties ); + $this->assertCount( 14, $properties ); + $this->assert_report_item_schema( $properties ); } /** @@ -141,5 +150,6 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $this->assertEquals( $test_customers[0]->get_id(), $reports[0]['user_id'] ); $this->assertEquals( 1, $reports[0]['orders_count'] ); $this->assertEquals( 100, $reports[0]['total_spend'] ); + $this->assert_report_item_schema( $reports[0] ); } } From 714c1ee030d87cf552b2e8397801c5f26120c518 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 17:45:28 -0700 Subject: [PATCH 22/33] =?UTF-8?q?Customer=20report=20data=20store:=20fix?= =?UTF-8?q?=20comparison=20operator=20for=20date=20param=20=E2=80=9Cafter?= =?UTF-8?q?=E2=80=9D=20where=20clause.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data-stores/class-wc-admin-reports-customers-data-store.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 7f14f062cb8..7dccd57d5e5 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -135,7 +135,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store if ( ! empty( $query_args[ $after_arg ] ) ) { $datetime = new DateTime( $query_args[ $after_arg ] ); $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $subclauses[] = "{$table_name}.{$column_name} <= '$datetime_str'"; + $subclauses[] = "{$table_name}.{$column_name} >= '$datetime_str'"; } if ( $subclauses ) { From fa3379e62ff6f5dd92dc2b02de1d6985a3e22766 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 18:55:48 -0700 Subject: [PATCH 23/33] =?UTF-8?q?Customer=20report=20data=20store:=20fix?= =?UTF-8?q?=20handling=20of=20=E2=80=98name=E2=80=99=20parameter=20in=20WH?= =?UTF-8?q?ERE=20clause.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../class-wc-admin-reports-customers-data-store.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 7dccd57d5e5..344dc1a0318 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -168,7 +168,6 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $where_clauses = array(); $exact_match_params = array( - 'name', 'username', 'email', 'country', @@ -183,6 +182,10 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } } + if ( ! empty( $query_args['name'] ) ) { + $where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) = %s", $query_args['name'] ); + } + $numeric_params = array( 'orders_count' => '%d', 'total_spend' => '%f', From d000d3e42a1ea1116644dd96c59db0e0bade7724 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 20 Dec 2018 18:56:36 -0700 Subject: [PATCH 24/33] =?UTF-8?q?Add=20cases=20using=20=E2=80=98name?= =?UTF-8?q?=E2=80=99=20and=20a=20date=20parameter=20to=20customers=20repor?= =?UTF-8?q?t=20endpoint=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/api/reports-customers.php | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/tests/api/reports-customers.php b/plugins/woocommerce-admin/tests/api/reports-customers.php index 84064a9bdbf..2223082f577 100644 --- a/plugins/woocommerce-admin/tests/api/reports-customers.php +++ b/plugins/woocommerce-admin/tests/api/reports-customers.php @@ -142,7 +142,7 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $headers = $response->get_headers(); $this->assertEquals( 200, $response->get_status() ); - $this->assertEquals( 5, count( $reports ) ); + $this->assertCount( 5, $reports ); $this->assertArrayHasKey( 'X-WP-Total', $headers ); $this->assertEquals( 10, $headers['X-WP-Total'] ); $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); @@ -151,5 +151,33 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $this->assertEquals( 1, $reports[0]['orders_count'] ); $this->assertEquals( 100, $reports[0]['total_spend'] ); $this->assert_report_item_schema( $reports[0] ); + + // Test name parameter (case with no matches). + $request->set_query_params( + array( + 'name' => 'Nota Customername', + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 0, $reports ); + + // Test name and last_order parameters. + $request->set_query_params( + array( + 'name' => 'Justin', + 'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z', + ) + ); + $response = $this->server->dispatch( $request ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $reports ); + $this->assertEquals( $test_customers[0]->get_id(), $reports[0]['user_id'] ); + $this->assertEquals( 1, $reports[0]['orders_count'] ); + $this->assertEquals( 100, $reports[0]['total_spend'] ); } } From c337944cf625ca6e2d1c8b6c8c138ac7738763a8 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Fri, 21 Dec 2018 13:12:58 -0700 Subject: [PATCH 25/33] Backfill guests into customer report lookup table using order data and billing email. --- .../includes/class-wc-admin-api-init.php | 43 ++++++- ...-wc-admin-reports-customers-data-store.php | 121 ++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 6ffabdefef3..61cdb42ab6e 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -386,7 +386,48 @@ class WC_Admin_Api_Init { } } - // TODO: Backfill customer lookup table with guests. + // Backfill customer lookup table with guests. + $guest_order_ids = get_transient( 'wc_update_350_all_guest_orders' ); + + if ( false === $guest_order_ids ) { + $guest_order_ids = wc_get_orders( + // TODO: restrict to certain order status? + array( + 'fields' => 'ids', + 'customer_id' => 0, + 'order' => 'asc', + 'orderby' => 'date', + 'posts_per_page' => -1, + ) + ); + + set_transient( 'wc_update_350_all_guest_orders', $guest_order_ids, DAY_IN_SECONDS ); + } + + $customers_report_data_store = new WC_Admin_Reports_Customers_Data_Store(); + + foreach ( $guest_order_ids as $idx => $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + unset( $guest_order_ids[ $idx ] ); + } elseif ( ! $order->get_billing_email( 'edit' ) ) { + unset( $guest_order_ids[ $idx ] ); + } else { + $result = $customers_report_data_store->update_guest_customer_by_order( $order ); + + if ( $result ) { + unset( $guest_order_ids[ $idx ] ); + } + } + + if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) { + // Update the transient for the next run to avoid processing the same orders again. + set_transient( 'wc_update_350_all_guest_orders', $guest_order_ids, DAY_IN_SECONDS ); + return true; + } + } + return true; } diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 344dc1a0318..e1b5236f6fc 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -336,6 +336,127 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store self::update_registered_customer( $customer_id ); } + /** + * Updates a guest (no user_id) customer data with new order data. + * + * @param WC_Order $order Order to update guest customer data with. + * @return int|false The number of rows affected, or false on error. + */ + public function update_guest_customer_by_order( $order ) { + global $wpdb; + + $email = $order->get_billing_email( 'edit' ); + + if ( empty( $email ) ) { + return true; + } + + $existing_guest = $this->get_guest_by_email( $email ); + + if ( $existing_guest ) { + $order_timestamp = date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ); + $new_orders_count = $existing_guest['orders_count'] + 1; + $new_total_spend = $existing_guest['total_spend'] + (float) $order->get_total( 'edit' ); + + return $wpdb->update( + $wpdb->prefix . self::TABLE_NAME, + array( + 'orders_count' => $new_orders_count, + 'total_spend' => $new_total_spend, + 'avg_order_value' => $new_total_spend / $new_orders_count, + 'date_last_order' => $order_timestamp, + 'date_last_active' => $order_timestamp, + ), + array( + 'customer_id' => $existing_guest['customer_id'], + ), + array( '%d', '%f', '%f', '%s', '%s' ), + array( '%d' ) + ); + } + + return $this->insert_guest_customer( + array( + 'first_name' => $order->get_billing_first_name( 'edit' ), + 'last_name' => $order->get_billing_last_name( 'edit' ), + 'email' => $email, + 'city' => $order->get_billing_city( 'edit' ), + 'postcode' => $order->get_billing_postcode( 'edit' ), + 'country' => $order->get_billing_country( 'edit' ), + 'orders_count' => 1, + 'total_spend' => (float) $order->get_total( 'edit' ), + 'avg_order_value' => (float) $order->get_total( 'edit' ), + 'date_last_order' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'date_last_active' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + ) + ); + } + + /** + * Insert a guest (no user_id) customer into lookup table. + * + * @param array $customer_data Array of guest customer data to insert. + * @return int|false The number of rows affected, or false on error. + */ + public static function insert_guest_customer( $customer_data ) { + global $wpdb; + + return $wpdb->insert( + $wpdb->prefix . self::TABLE_NAME, + array( + 'first_name' => $customer_data['first_name'], + 'last_name' => $customer_data['last_name'], + 'email' => $customer_data['email'], + 'city' => $customer_data['city'], + 'postcode' => $customer_data['postcode'], + 'country' => $customer_data['country'], + 'orders_count' => $customer_data['orders_count'], + 'total_spend' => $customer_data['total_spend'], + 'avg_order_value' => $customer_data['avg_order_value'], + 'date_last_order' => $customer_data['date_last_order'], + 'date_last_active' => $customer_data['date_last_active'], + ), + array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%f', + '%f', + '%s', + '%s', + ) + ); + } + + /** + * Retrieve a guest (no user_id) customer row by email. + * + * @param string $email Email address. + * @returns false|array Customer array if found, boolean false if not. + */ + public function get_guest_by_email( $email ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + $guest_row = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$table_name} WHERE email = %s AND user_id IS NULL LIMIT 1", + $email + ), + ARRAY_A + ); // WPCS: unprepared SQL ok. + + if ( $guest_row ) { + return $this->cast_numbers( $guest_row ); + } + + return false; + } + /** * Update the database with customer data. * From a1317f21520ed77939abdd3e66cdec240795a91a Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 3 Jan 2019 11:51:58 -0700 Subject: [PATCH 26/33] Customer lookup table: allow registration date to be null (for guests). --- plugins/woocommerce-admin/includes/class-wc-admin-api-init.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 61cdb42ab6e..1c205f205ef 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -582,7 +582,7 @@ class WC_Admin_Api_Init { avg_order_value double DEFAULT 0 NOT NULL, date_last_active timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, date_last_order timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, - date_registered timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, + date_registered timestamp NULL default null, country char(2) DEFAULT '' NOT NULL, postcode varchar(20) DEFAULT '' NOT NULL, city varchar(100) DEFAULT '' NOT NULL, From d42e04cd59b875814f3e699c1b5c001c13132157 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Thu, 3 Jan 2019 18:26:08 -0700 Subject: [PATCH 27/33] Short circuit registered customer data update when the given ID is bad. --- .../class-wc-admin-reports-customers-data-store.php | 7 ++++++- .../woocommerce-admin/tests/api/reports-customers.php | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index e1b5236f6fc..0ce1e67797a 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -466,7 +466,12 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store public static function update_registered_customer( $user_id ) { global $wpdb; - $customer = new WC_Customer( $user_id ); + $customer = new WC_Customer( $user_id ); + + if ( $customer->get_id() != $user_id ) { + return false; + } + $order_count = $customer->get_order_count( 'edit' ); $total_spend = $customer->get_total_spent( 'edit' ); $last_order = $customer->get_last_order(); diff --git a/plugins/woocommerce-admin/tests/api/reports-customers.php b/plugins/woocommerce-admin/tests/api/reports-customers.php index 2223082f577..dfd8a7b3d3d 100644 --- a/plugins/woocommerce-admin/tests/api/reports-customers.php +++ b/plugins/woocommerce-admin/tests/api/reports-customers.php @@ -96,6 +96,16 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case { $this->assertEquals( 401, $response->get_status() ); } + /** + * Test calling update_registered_customer() with a bad user id. + * + * @since 3.5.0 + */ + public function test_update_registered_customer_with_bad_user_id() { + $result = WC_Admin_Reports_Customers_Data_Store::update_registered_customer( 2 ); + $this->assertFalse( $result ); + } + /** * Test getting reports. * From 1a90840e977ea3e5bbe4892ea6757bed747d2730 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Mon, 7 Jan 2019 18:21:36 -0700 Subject: [PATCH 28/33] Add customer_id to order stats report table to reference with customer lookup. --- plugins/woocommerce-admin/includes/class-wc-admin-api-init.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 1c205f205ef..019b362aad4 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -508,6 +508,7 @@ class WC_Admin_Api_Init { net_total double DEFAULT 0 NOT NULL, returning_customer boolean DEFAULT 0 NOT NULL, status varchar(200) NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (order_id), KEY date_created (date_created) ) $collate; From 8105ddb38ac45aa6dc63cb56668c452a84473cd1 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Mon, 7 Jan 2019 18:33:40 -0700 Subject: [PATCH 29/33] Add new guest customers to lookup when syncing the order stats report table. --- .../includes/class-wc-admin-api-init.php | 55 +------- ...-wc-admin-reports-customers-data-store.php | 128 +++++------------- ...ass-wc-admin-reports-orders-data-store.php | 57 +++++--- 3 files changed, 78 insertions(+), 162 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 019b362aad4..8a739984b31 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -29,8 +29,6 @@ class WC_Admin_Api_Init { // Initialize Orders data store class's static vars. add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 ); - // Initialize Customers Report data store class's static vars. - add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ), 20 ); } /** @@ -265,9 +263,11 @@ class WC_Admin_Api_Init { * Regenerate data for reports. */ public static function regenerate_report_data() { + // Add registered customers to the lookup table before updating order stats + // so that the orders can be associated with the `customer_id` column. + self::customer_lookup_store_init(); WC_Admin_Reports_Orders_Data_Store::queue_order_stats_repopulate_database(); self::order_product_lookup_store_init(); - self::customer_lookup_store_init(); } /** @@ -336,13 +336,6 @@ class WC_Admin_Api_Init { return true; } - /** - * Init customers report data store. - */ - public static function customers_report_data_store_init() { - WC_Admin_Reports_Customers_Data_Store::init(); - } - /** * Init customer lookup store. * @@ -386,48 +379,6 @@ class WC_Admin_Api_Init { } } - // Backfill customer lookup table with guests. - $guest_order_ids = get_transient( 'wc_update_350_all_guest_orders' ); - - if ( false === $guest_order_ids ) { - $guest_order_ids = wc_get_orders( - // TODO: restrict to certain order status? - array( - 'fields' => 'ids', - 'customer_id' => 0, - 'order' => 'asc', - 'orderby' => 'date', - 'posts_per_page' => -1, - ) - ); - - set_transient( 'wc_update_350_all_guest_orders', $guest_order_ids, DAY_IN_SECONDS ); - } - - $customers_report_data_store = new WC_Admin_Reports_Customers_Data_Store(); - - foreach ( $guest_order_ids as $idx => $order_id ) { - $order = wc_get_order( $order_id ); - - if ( ! $order ) { - unset( $guest_order_ids[ $idx ] ); - } elseif ( ! $order->get_billing_email( 'edit' ) ) { - unset( $guest_order_ids[ $idx ] ); - } else { - $result = $customers_report_data_store->update_guest_customer_by_order( $order ); - - if ( $result ) { - unset( $guest_order_ids[ $idx ] ); - } - } - - if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) { - // Update the transient for the next run to avoid processing the same orders again. - set_transient( 'wc_update_350_all_guest_orders', $guest_order_ids, DAY_IN_SECONDS ); - return true; - } - } - return true; } diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 0ce1e67797a..6d85ad5d4e8 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -55,15 +55,6 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store 'avg_order_value' => 'avg_order_value', ); - /** - * Set up all the hooks for maintaining and populating table data. - */ - public static function init() { - add_action( 'woocommerce_new_order', array( __CLASS__, 'sync_order' ) ); - add_action( 'woocommerce_update_order', array( __CLASS__, 'sync_order' ) ); - add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) ); - } - /** * Maps ordering specified by the user to columns in the database/fields in the data. * @@ -314,68 +305,29 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } /** - * Add customer information to the lookup table when orders are created or modified. + * Gets the guest (no user_id) customer ID or creates a new one for + * the corresponding billing email in the provided WC_Order * - * @param int $post_id Post ID. + * @param WC_Order $order Order to get/create guest customer data with. + * @return int|false The ID of the retrieved/created customer, or false on error. */ - public static function sync_order( $post_id ) { - if ( 'shop_order' !== get_post_type( $post_id ) ) { - return; - } - - $order = wc_get_order( $post_id ); - if ( ! $order ) { - return; - } - - $customer_id = $order->get_customer_id(); - if ( 0 === $customer_id ) { - return; - } - - self::update_registered_customer( $customer_id ); - } - - /** - * Updates a guest (no user_id) customer data with new order data. - * - * @param WC_Order $order Order to update guest customer data with. - * @return int|false The number of rows affected, or false on error. - */ - public function update_guest_customer_by_order( $order ) { + public function get_or_create_guest_customer_from_order( $order ) { global $wpdb; $email = $order->get_billing_email( 'edit' ); if ( empty( $email ) ) { - return true; + return false; } $existing_guest = $this->get_guest_by_email( $email ); if ( $existing_guest ) { - $order_timestamp = date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ); - $new_orders_count = $existing_guest['orders_count'] + 1; - $new_total_spend = $existing_guest['total_spend'] + (float) $order->get_total( 'edit' ); - - return $wpdb->update( - $wpdb->prefix . self::TABLE_NAME, - array( - 'orders_count' => $new_orders_count, - 'total_spend' => $new_total_spend, - 'avg_order_value' => $new_total_spend / $new_orders_count, - 'date_last_order' => $order_timestamp, - 'date_last_active' => $order_timestamp, - ), - array( - 'customer_id' => $existing_guest['customer_id'], - ), - array( '%d', '%f', '%f', '%s', '%s' ), - array( '%d' ) - ); + return $existing_guest['customer_id']; } - return $this->insert_guest_customer( + $result = $wpdb->insert( + $wpdb->prefix . self::TABLE_NAME, array( 'first_name' => $order->get_billing_first_name( 'edit' ), 'last_name' => $order->get_billing_last_name( 'edit' ), @@ -383,38 +335,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store 'city' => $order->get_billing_city( 'edit' ), 'postcode' => $order->get_billing_postcode( 'edit' ), 'country' => $order->get_billing_country( 'edit' ), - 'orders_count' => 1, - 'total_spend' => (float) $order->get_total( 'edit' ), - 'avg_order_value' => (float) $order->get_total( 'edit' ), - 'date_last_order' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), 'date_last_active' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), - ) - ); - } - - /** - * Insert a guest (no user_id) customer into lookup table. - * - * @param array $customer_data Array of guest customer data to insert. - * @return int|false The number of rows affected, or false on error. - */ - public static function insert_guest_customer( $customer_data ) { - global $wpdb; - - return $wpdb->insert( - $wpdb->prefix . self::TABLE_NAME, - array( - 'first_name' => $customer_data['first_name'], - 'last_name' => $customer_data['last_name'], - 'email' => $customer_data['email'], - 'city' => $customer_data['city'], - 'postcode' => $customer_data['postcode'], - 'country' => $customer_data['country'], - 'orders_count' => $customer_data['orders_count'], - 'total_spend' => $customer_data['total_spend'], - 'avg_order_value' => $customer_data['avg_order_value'], - 'date_last_order' => $customer_data['date_last_order'], - 'date_last_active' => $customer_data['date_last_active'], ), array( '%s', @@ -423,13 +344,11 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store '%s', '%s', '%s', - '%d', - '%f', - '%f', - '%s', '%s', ) ); + + return $result ? $wpdb->insert_id : false; } /** @@ -457,6 +376,31 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store return false; } + /** + * Retrieve a registered customer row by user_id. + * + * @param string|int $user_id User ID. + * @returns false|array Customer array if found, boolean false if not. + */ + public function get_customer_by_user_id( $user_id ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + $customer = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$table_name} WHERE user_id = %d LIMIT 1", + $user_id + ), + ARRAY_A + ); // WPCS: unprepared SQL ok. + + if ( $customer ) { + return $this->cast_numbers( $customer ); + } + + return false; + } + /** * Update the database with customer data. * diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index a56ec86b361..7a353c3f991 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -447,7 +447,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp return; } - $data = array( + $data = array( 'order_id' => $order->get_id(), 'date_created' => $order->get_date_created()->date( 'Y-m-d H:i:s' ), 'num_items_sold' => self::get_num_items_sold( $order ), @@ -460,25 +460,46 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'returning_customer' => self::is_returning_customer( $order ), 'status' => self::normalize_order_status( $order->get_status() ), ); + $format = array( + '%d', + '%s', + '%d', + '%f', + '%f', + '%f', + '%f', + '%f', + '%f', + '%d', + '%s', + ); + + // Ensure we're associating this order with a Customer in the lookup table. + $order_user_id = $order->get_customer_id(); + $customers_data_store = new WC_Admin_Reports_Customers_Data_Store(); + + if ( 0 === $order_user_id ) { + $email = $order->get_billing_email( 'edit' ); + + if ( $email ) { + $customer_id = $customers_data_store->get_or_create_guest_customer_from_order( $order ); + + if ( $customer_id ) { + $data['customer_id'] = $customer_id; + $format[] = '%d'; + } + } + } else { + $customer = $customers_data_store->get_customer_by_user_id( $order_user_id ); + + if ( $customer && $customer['customer_id'] ) { + $data['customer_id'] = $customer['customer_id']; + $format[] = '%d'; + } + } // Update or add the information to the DB. - return $wpdb->replace( - $table_name, - $data, - array( - '%d', - '%s', - '%d', - '%f', - '%f', - '%f', - '%f', - '%f', - '%f', - '%d', - '%s', - ) - ); + return $wpdb->replace( $table_name, $data, $format ); } /** From 154482acc9b48d5d586f5866b896b89feb2f9ffb Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Mon, 7 Jan 2019 20:54:17 -0700 Subject: [PATCH 30/33] Derive orders_count, total_spend, avg_order_value, and date_late_order for customer reports from the order stats report table. --- .../includes/class-wc-admin-api-init.php | 4 - ...-wc-admin-reports-customers-data-store.php | 137 ++++++++++++------ 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 8a739984b31..fcc02280252 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -529,11 +529,7 @@ class WC_Admin_Api_Init { first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, email varchar(100) NOT NULL, - orders_count BIGINT UNSIGNED NOT NULL, - total_spend double DEFAULT 0 NOT NULL, - avg_order_value double DEFAULT 0 NOT NULL, date_last_active timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, - date_last_order timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, date_registered timestamp NULL default null, country char(2) DEFAULT '' NOT NULL, postcode varchar(20) DEFAULT '' NOT NULL, diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 6d85ad5d4e8..65a5edbe3b2 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -50,11 +50,22 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store 'postcode' => 'postcode', 'date_registered' => 'date_registered', 'date_last_active' => 'date_last_active', - 'orders_count' => 'orders_count', - 'total_spend' => 'total_spend', - 'avg_order_value' => 'avg_order_value', + 'orders_count' => 'COUNT( order_id ) as orders_count', + 'total_spend' => 'SUM( gross_total ) as total_spend', + 'avg_order_value' => '( SUM( gross_total ) / COUNT( order_id ) ) as avg_order_value', ); + /** + * Constructor. + */ + public function __construct() { + global $wpdb; + + // Initialize some report columns that need disambiguation. + $this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id'; + $this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order"; + } + /** * Maps ordering specified by the user to columns in the database/fields in the data. * @@ -99,45 +110,66 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store * @return array */ protected function get_time_period_sql_params( $query_args, $table_name ) { + global $wpdb; + $sql_query = array( - 'from_clause' => '', 'where_time_clause' => '', 'where_clause' => '', + 'having_clause' => '', ); $date_param_mapping = array( - 'registered' => 'date_registered', - 'last_active' => 'date_last_active', - 'last_order' => 'date_last_order', + 'registered' => array( + 'clause' => 'where', + 'column' => $table_name . '.date_registered', + ), + 'last_active' => array( + 'clause' => 'where', + 'column' => $table_name . '.date_last_active', + ), + 'last_order' => array( + 'clause' => 'having', + 'column' => "MAX( {$wpdb->prefix}wc_order_stats.date_created )", + ), ); $match_operator = $this->get_match_operator( $query_args ); $where_time_clauses = array(); + $having_time_clauses = array(); - foreach ( $date_param_mapping as $query_param => $column_name ) { - $subclauses = array(); - $before_arg = $query_param . '_before'; - $after_arg = $query_param . '_after'; + foreach ( $date_param_mapping as $query_param => $param_info ) { + $subclauses = array(); + $before_arg = $query_param . '_before'; + $after_arg = $query_param . '_after'; + $column_name = $param_info['column']; if ( ! empty( $query_args[ $before_arg ] ) ) { $datetime = new DateTime( $query_args[ $before_arg ] ); $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $subclauses[] = "{$table_name}.{$column_name} <= '$datetime_str'"; + $subclauses[] = "{$column_name} <= '$datetime_str'"; } if ( ! empty( $query_args[ $after_arg ] ) ) { $datetime = new DateTime( $query_args[ $after_arg ] ); $datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format ); - $subclauses[] = "{$table_name}.{$column_name} >= '$datetime_str'"; + $subclauses[] = "{$column_name} >= '$datetime_str'"; } - if ( $subclauses ) { + if ( $subclauses && ( 'where' === $param_info['clause'] ) ) { $where_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')'; } + + if ( $subclauses && ( 'having' === $param_info['clause'] ) ) { + $having_time_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')'; + } } if ( $where_time_clauses ) { $sql_query['where_time_clause'] = ' AND ' . implode( " {$match_operator} ", $where_time_clauses ); } + if ( $having_time_clauses ) { + $sql_query['having_clause'] = ' AND ' . implode( " {$match_operator} ", $having_time_clauses ); + } + return $sql_query; } @@ -149,14 +181,15 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store */ protected function get_sql_query_params( $query_args ) { global $wpdb; - $order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME; + $customer_lookup_table = $wpdb->prefix . self::TABLE_NAME; - $sql_query_params = $this->get_time_period_sql_params( $query_args, $order_product_lookup_table ); + $sql_query_params = $this->get_time_period_sql_params( $query_args, $customer_lookup_table ); $sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) ); $sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) ); $match_operator = $this->get_match_operator( $query_args ); $where_clauses = array(); + $having_clauses = array(); $exact_match_params = array( 'username', @@ -167,7 +200,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store foreach ( $exact_match_params as $exact_match_param ) { if ( ! empty( $query_args[ $exact_match_param ] ) ) { $where_clauses[] = $wpdb->prepare( - "{$order_product_lookup_table}.{$exact_match_param} = %s", + "{$customer_lookup_table}.{$exact_match_param} = %s", $query_args[ $exact_match_param ] ); // WPCS: unprepared SQL ok. } @@ -178,32 +211,41 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } $numeric_params = array( - 'orders_count' => '%d', - 'total_spend' => '%f', - 'avg_order_value' => '%f', + 'orders_count' => array( + 'column' => 'COUNT( order_id )', + 'format' => '%d', + ), + 'total_spend' => array( + 'column' => 'SUM( gross_total )', + 'format' => '%f', + ), + 'avg_order_value' => array( + 'column' => '( SUM( gross_total ) / COUNT( order_id ) )', + 'format' => '%f', + ), ); - foreach ( $numeric_params as $numeric_param => $sql_format ) { + foreach ( $numeric_params as $numeric_param => $param_info ) { $subclauses = array(); $min_param = $numeric_param . '_min'; $max_param = $numeric_param . '_max'; if ( isset( $query_args[ $min_param ] ) ) { $subclauses[] = $wpdb->prepare( - "{$order_product_lookup_table}.{$numeric_param} >= {$sql_format}", + "{$param_info['column']} >= {$param_info['format']}", $query_args[ $min_param ] ); // WPCS: unprepared SQL ok. } if ( isset( $query_args[ $max_param ] ) ) { $subclauses[] = $wpdb->prepare( - "{$order_product_lookup_table}.{$numeric_param} <= {$sql_format}", + "{$param_info['column']} <= {$param_info['format']}", $query_args[ $max_param ] ); // WPCS: unprepared SQL ok. } if ( $subclauses ) { - $where_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')'; + $having_clauses[] = '(' . implode( ' AND ', $subclauses ) . ')'; } } @@ -212,6 +254,11 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $sql_query_params['where_clause'] = $preceding_match . implode( " {$match_operator} ", $where_clauses ); } + if ( $having_clauses ) { + $preceding_match = empty( $sql_query_params['having_clause'] ) ? ' AND ' : " {$match_operator} "; + $sql_query_params['having_clause'] .= $preceding_match . implode( " {$match_operator} ", $having_clauses ); + } + return $sql_query_params; } @@ -224,7 +271,8 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store public function get_data( $query_args ) { global $wpdb; - $table_name = $wpdb->prefix . self::TABLE_NAME; + $customers_table_name = $wpdb->prefix . self::TABLE_NAME; + $order_stats_table_name = $wpdb->prefix . 'wc_order_stats'; // These defaults are only partially applied when used via REST API, as that has its own defaults. $defaults = array( @@ -251,14 +299,24 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $sql_query_params = $this->get_sql_query_params( $query_args ); $db_records_count = (int) $wpdb->get_var( - "SELECT COUNT(*) + "SELECT COUNT(*) FROM ( + SELECT {$customers_table_name}.customer_id FROM - {$table_name} - {$sql_query_params['from_clause']} + {$customers_table_name} + JOIN + {$order_stats_table_name} + ON + {$customers_table_name}.customer_id = {$order_stats_table_name}.customer_id WHERE 1=1 {$sql_query_params['where_time_clause']} {$sql_query_params['where_clause']} + GROUP BY + {$customers_table_name}.customer_id + HAVING + 1=1 + {$sql_query_params['having_clause']} + ) as tt " ); // WPCS: cache ok, DB call ok, unprepared SQL ok. @@ -271,14 +329,20 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store "SELECT {$selections} FROM - {$table_name} - {$sql_query_params['from_clause']} + {$customers_table_name} + JOIN + {$order_stats_table_name} + ON + {$customers_table_name}.customer_id = {$order_stats_table_name}.customer_id WHERE 1=1 {$sql_query_params['where_time_clause']} {$sql_query_params['where_clause']} GROUP BY - customer_id + {$customers_table_name}.customer_id + HAVING + 1=1 + {$sql_query_params['having_clause']} ORDER BY {$sql_query_params['order_by_clause']} {$sql_query_params['limit']} @@ -416,9 +480,6 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store return false; } - $order_count = $customer->get_order_count( 'edit' ); - $total_spend = $customer->get_total_spent( 'edit' ); - $last_order = $customer->get_last_order(); $last_active = $customer->get_meta( 'wc_last_active', true, 'edit' ); // TODO: try to preserve customer_id for existing user_id? @@ -433,10 +494,6 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store 'city' => $customer->get_billing_city( 'edit' ), 'postcode' => $customer->get_billing_postcode( 'edit' ), 'country' => $customer->get_billing_country( 'edit' ), - 'orders_count' => $order_count, - 'total_spend' => $total_spend, - 'avg_order_value' => $order_count ? ( $total_spend / $order_count ) : 0, - 'date_last_order' => $last_order ? date( 'Y-m-d H:i:s', $last_order->get_date_created( 'edit' )->getTimestamp() ) : '', 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), 'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', ), @@ -449,10 +506,6 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store '%s', '%s', '%s', - '%d', - '%f', - '%f', - '%s', '%s', '%s', ) From 46b503dd5444a49d702d5119a4425e768c5ac28e Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Tue, 8 Jan 2019 10:18:25 -0700 Subject: [PATCH 31/33] Update customers report lookup table when customers/users are updated. --- .../includes/class-wc-admin-api-init.php | 11 ++ ...-wc-admin-reports-customers-data-store.php | 105 +++++++++++++----- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index fcc02280252..cfbf905d45c 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -29,6 +29,10 @@ class WC_Admin_Api_Init { // Initialize Orders data store class's static vars. add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 ); + // Initialize Customers Report data store sync hooks. + // Note: we need to hook into 'wp' before `wc_current_user_is_active`. + // See: https://github.com/woocommerce/woocommerce/blob/942615101ba00c939c107c3a4820c3d466864872/includes/wc-user-functions.php#L749. + add_action( 'wp', array( 'WC_Admin_Api_Init', 'customers_report_data_store_init' ), 9 ); } /** @@ -336,6 +340,13 @@ class WC_Admin_Api_Init { return true; } + /** + * Init customers report data store. + */ + public static function customers_report_data_store_init() { + WC_Admin_Reports_Customers_Data_Store::init(); + } + /** * Init customer lookup store. * diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 65a5edbe3b2..f2e792a47fd 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -66,6 +66,30 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order"; } + /** + * Set up all the hooks for maintaining and populating table data. + */ + public static function init() { + add_action( 'woocommerce_new_customer', array( __CLASS__, 'update_registered_customer' ) ); + add_action( 'woocommerce_update_customer', array( __CLASS__, 'update_registered_customer' ) ); + add_action( 'edit_user_profile_update', array( __CLASS__, 'update_registered_customer' ) ); + add_action( 'updated_user_meta', array( __CLASS__, 'update_registered_customer_via_last_active' ), 10, 3 ); + } + + /** + * Trigger a customer update if their "last active" meta value was changed. + * Function expects to be hooked into the `updated_user_meta` action. + * + * @param int $meta_id ID of updated metadata entry. + * @param int $user_id ID of the user being updated. + * @param string $meta_key Meta key being updated. + */ + public static function update_registered_customer_via_last_active( $meta_id, $user_id, $meta_key ) { + if ( 'wc_last_active' === $meta_key ) { + self::update_registered_customer( $user_id ); + } + } + /** * Maps ordering specified by the user to columns in the database/fields in the data. * @@ -465,6 +489,26 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store return false; } + /** + * Retrieve a registered customer row id by user_id. + * + * @param string|int $user_id User ID. + * @returns false|int Customer ID if found, boolean false if not. + */ + public static function get_customer_id_by_user_id( $user_id ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + $customer_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT customer_id FROM {$table_name} WHERE user_id = %d LIMIT 1", + $user_id + ) + ); // WPCS: unprepared SQL ok. + + return $customer_id ? (int) $customer_id : false; + } + /** * Update the database with customer data. * @@ -481,35 +525,40 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } $last_active = $customer->get_meta( 'wc_last_active', true, 'edit' ); - - // TODO: try to preserve customer_id for existing user_id? - return $wpdb->replace( - $wpdb->prefix . self::TABLE_NAME, - array( - 'user_id' => $user_id, - 'username' => $customer->get_username( 'edit' ), - 'first_name' => $customer->get_first_name( 'edit' ), - 'last_name' => $customer->get_last_name( 'edit' ), - 'email' => $customer->get_email( 'edit' ), - 'city' => $customer->get_billing_city( 'edit' ), - 'postcode' => $customer->get_billing_postcode( 'edit' ), - 'country' => $customer->get_billing_country( 'edit' ), - 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), - 'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', - ), - array( - '%d', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - ) + $data = array( + 'user_id' => $user_id, + 'username' => $customer->get_username( 'edit' ), + 'first_name' => $customer->get_first_name( 'edit' ), + 'last_name' => $customer->get_last_name( 'edit' ), + 'email' => $customer->get_email( 'edit' ), + 'city' => $customer->get_billing_city( 'edit' ), + 'postcode' => $customer->get_billing_postcode( 'edit' ), + 'country' => $customer->get_billing_country( 'edit' ), + 'date_registered' => date( 'Y-m-d H:i:s', $customer->get_date_created( 'edit' )->getTimestamp() ), + 'date_last_active' => $last_active ? date( 'Y-m-d H:i:s', $last_active ) : '', ); + $format = array( + '%d', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + ); + + $customer_id = self::get_customer_id_by_user_id( $user_id ); + + if ( $customer_id ) { + // Preserve customer_id for existing user_id. + $data['customer_id'] = $customer_id; + $format[] = '%d'; + } + + return $wpdb->replace( $wpdb->prefix . self::TABLE_NAME, $data, $format ); } /** From d9fd8568dff9084cc612b5dd96a9d52120cfa2a3 Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Tue, 8 Jan 2019 10:47:19 -0700 Subject: [PATCH 32/33] Customers report: perform a LEFT JOIN on the order stats table to include customers that have not yet placed an order. --- .../class-wc-admin-reports-customers-data-store.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index f2e792a47fd..5d99eca68fc 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -327,7 +327,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store SELECT {$customers_table_name}.customer_id FROM {$customers_table_name} - JOIN + LEFT JOIN {$order_stats_table_name} ON {$customers_table_name}.customer_id = {$order_stats_table_name}.customer_id @@ -354,7 +354,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store {$selections} FROM {$customers_table_name} - JOIN + LEFT JOIN {$order_stats_table_name} ON {$customers_table_name}.customer_id = {$order_stats_table_name}.customer_id From 06763cc3a5c0bc9accfd4a1563a5019013481a0c Mon Sep 17 00:00:00 2001 From: Jeff Stieler Date: Tue, 8 Jan 2019 18:49:49 -0700 Subject: [PATCH 33/33] =?UTF-8?q?Don=E2=80=99t=20cast=20date=20values=20to?= =?UTF-8?q?=20strings=20in=20customers=20report=20data=20store=20since=20t?= =?UTF-8?q?hey=E2=80=99re=20allowed=20to=20be=20null.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NULL column values resulted in NOW() in the response. --- .../data-stores/class-wc-admin-reports-customers-data-store.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 5d99eca68fc..16302ada7ef 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php @@ -27,8 +27,6 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store protected $column_types = array( 'customer_id' => 'intval', 'user_id' => 'intval', - 'date_registered' => 'strval', - 'date_last_active' => 'strval', 'orders_count' => 'intval', 'total_spend' => 'floatval', 'avg_order_value' => 'floatval',