diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-orders-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-orders-stats-controller.php similarity index 94% rename from plugins/woocommerce-admin/includes/api/class-wc-admin-rest-orders-controller.php rename to plugins/woocommerce-admin/includes/api/class-wc-admin-rest-orders-stats-controller.php index d6c28ed095a..acaacf392be 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-orders-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-orders-stats-controller.php @@ -15,7 +15,7 @@ defined( 'ABSPATH' ) || exit; * @package WooCommerce Admin/API * @extends WC_REST_Orders_Controller */ -class WC_Admin_REST_Orders_Controller extends WC_REST_Orders_Controller { +class WC_Admin_REST_Orders_Stats_Controller extends WC_REST_Orders_Controller { /** * Get the query params for collections. * diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-categories-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-categories-controller.php index 3aa23d33d49..273e436b451 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-categories-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-categories-controller.php @@ -13,9 +13,9 @@ defined( 'ABSPATH' ) || exit; * REST API Reports categories controller class. * * @package WooCommerce/API - * @extends WC_REST_Reports_Controller + * @extends WC_Admin_REST_Reports_Controller */ -class WC_Admin_REST_Reports_Categories_Controller extends WC_REST_Reports_Controller { +class WC_Admin_REST_Reports_Categories_Controller extends WC_Admin_REST_Reports_Controller { /** * Endpoint namespace. @@ -317,18 +317,4 @@ class WC_Admin_REST_Reports_Categories_Controller extends WC_REST_Reports_Contro return $params; } - /** - * Get order statuses without prefixes. - * - * @return array - */ - protected function get_order_statuses() { - $order_statuses = array(); - - foreach ( array_keys( wc_get_order_statuses() ) as $status ) { - $order_statuses[] = str_replace( 'wc-', '', $status ); - } - - return $order_statuses; - } } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php index 83c9628168f..764e51d2d9b 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-controller.php @@ -224,4 +224,19 @@ class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller { 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ); } + + /** + * Get order statuses without prefixes. + * + * @return array + */ + public function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-controller.php new file mode 100644 index 00000000000..b0ca69ecceb --- /dev/null +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-controller.php @@ -0,0 +1,356 @@ +prepare_reports_query( $request ); + $orders_query = new WC_Admin_Reports_Orders_Query( $query_args ); + $report_data = $orders_query->get_data(); + + $data = array(); + + foreach ( $report_data->data as $orders_data ) { + $item = $this->prepare_item_for_response( $orders_data, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + $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 ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $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 WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $report, $request ) { + $data = $report; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $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 ) ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_orders', $response, $report, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Reports_Query $object Object data. + * @return array + */ + protected function prepare_links( $object ) { + $links = array( + 'order' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object['order_id'] ) ), + ), + ); + + return $links; + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_orders', + 'type' => 'object', + 'properties' => array( + 'order_id' => array( + 'description' => __( 'Order ID.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'Date the order was created.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Order status.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'Customer ID.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'num_items_sold' => array( + 'description' => __( 'Number of items sold.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'net_total' => array( + 'description' => __( 'Net total revenue.', 'wc-admin' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_type' => array( + 'description' => __( 'Returning or new customer.', 'wc-admin' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'extended_info' => array( + 'products' => array( + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'List of product IDs and names.', 'wc-admin' ), + ), + 'categories' => array( + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit' ), + 'description' => __( 'Category IDs.', 'wc-admin' ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'wc-admin' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'wc-admin' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $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', + 'enum' => array( + 'date', + 'num_items_sold', + 'net_total', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product_includes'] = array( + 'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product_excludes'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['coupon_includes'] = array( + 'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['coupon_excludes'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'wc-admin' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['status_is'] = array( + 'description' => __( 'Limit result set to items that have the specified order status.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_slug_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'enum' => $this->get_order_statuses(), + 'type' => 'string', + ), + ); + $params['status_is_not'] = array( + 'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'wc-admin' ), + 'type' => 'array', + 'sanitize_callback' => 'wp_parse_slug_list', + 'validate_callback' => 'rest_validate_request_arg', + 'items' => array( + 'enum' => $this->get_order_statuses(), + 'type' => 'string', + ), + ); + $params['customer_type'] = array( + 'description' => __( 'Limit result set to returning or new customers.', 'wc-admin' ), + 'type' => 'string', + 'default' => '', + 'enum' => array( + '', + 'returning', + 'new', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['extended_info'] = array( + 'description' => __( 'Add additional piece of info about each coupon to the report.', 'wc-admin' ), + 'type' => 'boolean', + 'default' => false, + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php index 592f4ec1290..f73c0aeed38 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php @@ -13,9 +13,9 @@ defined( 'ABSPATH' ) || exit; * REST API Reports orders stats controller class. * * @package WooCommerce/API - * @extends WC_REST_Reports_Controller + * @extends WC_Admin_REST_Reports_Controller */ -class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Controller { +class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Reports_Controller { /** * Endpoint namespace. @@ -384,18 +384,4 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont return $params; } - /** - * Get order statuses without prefixes. - * - * @return array - */ - protected function get_order_statuses() { - $order_statuses = array(); - - foreach ( array_keys( wc_get_order_statuses() ) as $status ) { - $order_statuses[] = str_replace( 'wc-', '', $status ); - } - - return $order_statuses; - } } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-system-status-tools-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-system-status-tools-controller.php index e99274c2e88..1cc3cceeaa8 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-system-status-tools-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-system-status-tools-controller.php @@ -55,7 +55,7 @@ class WC_Admin_REST_System_Status_Tools_Controller extends WC_REST_System_Status switch ( $tool ) { case 'rebuild_stats': - WC_Admin_Reports_Orders_Data_Store::queue_order_stats_repopulate_database(); + WC_Admin_Reports_Orders_Stats_Data_Store::queue_order_stats_repopulate_database(); $message = __( 'Rebuilding reports data in the background . . .', 'wc-admin' ); break; default: 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 3f4fd07624b..b995d195428 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -48,6 +48,7 @@ class WC_Admin_Api_Init { // Query classes for reports. require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-query.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-stats-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-query.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-variations-query.php'; @@ -64,6 +65,7 @@ class WC_Admin_Api_Init { // Data stores. require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-orders-data-store.php'; + require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-orders-stats-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-variations-data-store.php'; require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-stats-data-store.php'; @@ -92,7 +94,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-download-ips-controller.php'; - require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-orders-controller.php'; + require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-orders-stats-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-products-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-product-reviews-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php'; @@ -104,6 +106,7 @@ class WC_Admin_Api_Init { require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-files-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-stats-controller.php'; + require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-orders-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-orders-stats-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-products-controller.php'; require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-variations-controller.php'; @@ -121,7 +124,7 @@ class WC_Admin_Api_Init { 'WC_Admin_REST_Customers_Controller', 'WC_Admin_REST_Data_Controller', 'WC_Admin_REST_Data_Download_Ips_Controller', - 'WC_Admin_REST_Orders_Controller', + 'WC_Admin_REST_Orders_Stats_Controller', 'WC_Admin_REST_Products_Controller', 'WC_Admin_REST_Product_Reviews_Controller', 'WC_Admin_REST_Reports_Controller', @@ -130,6 +133,7 @@ class WC_Admin_Api_Init { 'WC_Admin_REST_Reports_Variations_Controller', 'WC_Admin_REST_Reports_Products_Stats_Controller', 'WC_Admin_REST_Reports_Revenue_Stats_Controller', + 'WC_Admin_REST_Reports_Orders_Controller', 'WC_Admin_REST_Reports_Orders_Stats_Controller', 'WC_Admin_REST_Reports_Categories_Controller', 'WC_Admin_REST_Reports_Taxes_Controller', @@ -203,9 +207,9 @@ class WC_Admin_Api_Init { && isset( $endpoints['/wc/v3/orders/(?P[\d]+)'][5] ) && isset( $endpoints['/wc/v3/orders/(?P[\d]+)'][4] ) && isset( $endpoints['/wc/v3/orders/(?P[\d]+)'][3] ) - && $endpoints['/wc/v3/orders/(?P[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller - && $endpoints['/wc/v3/orders/(?P[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Orders_Controller - && $endpoints['/wc/v3/orders/(?P[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Orders_Controller + && $endpoints['/wc/v3/orders/(?P[\d]+)'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller + && $endpoints['/wc/v3/orders/(?P[\d]+)'][4]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller + && $endpoints['/wc/v3/orders/(?P[\d]+)'][5]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller ) { $endpoints['/wc/v3/orders/(?P[\d]+)'][0] = $endpoints['/wc/v3/orders/(?P[\d]+)'][3]; $endpoints['/wc/v3/orders/(?P[\d]+)'][1] = $endpoints['/wc/v3/orders/(?P[\d]+)'][4]; @@ -216,8 +220,8 @@ class WC_Admin_Api_Init { if ( isset( $endpoints['/wc/v3/orders'] ) && isset( $endpoints['/wc/v3/orders'][3] ) && isset( $endpoints['/wc/v3/orders'][2] ) - && $endpoints['/wc/v3/orders'][2]['callback'][0] instanceof WC_Admin_REST_Orders_Controller - && $endpoints['/wc/v3/orders'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Controller + && $endpoints['/wc/v3/orders'][2]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller + && $endpoints['/wc/v3/orders'][3]['callback'][0] instanceof WC_Admin_REST_Orders_Stats_Controller ) { $endpoints['/wc/v3/orders'][0] = $endpoints['/wc/v3/orders'][2]; $endpoints['/wc/v3/orders'][1] = $endpoints['/wc/v3/orders'][3]; @@ -277,7 +281,7 @@ class WC_Admin_Api_Init { // 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(); + WC_Admin_Reports_Orders_Stats_Data_Store::queue_order_stats_repopulate_database(); self::order_product_lookup_store_init(); } @@ -305,7 +309,7 @@ class WC_Admin_Api_Init { * Init orders data store. */ public static function orders_data_store_init() { - WC_Admin_Reports_Orders_Data_Store::init(); + WC_Admin_Reports_Orders_Stats_Data_Store::init(); WC_Admin_Reports_Products_Data_Store::init(); WC_Admin_Reports_Taxes_Data_Store::init(); WC_Admin_Reports_Coupons_Data_Store::init(); @@ -410,17 +414,18 @@ class WC_Admin_Api_Init { return array_merge( $data_stores, array( - 'report-revenue-stats' => 'WC_Admin_Reports_Orders_Data_Store', - 'report-orders-stats' => 'WC_Admin_Reports_Orders_Data_Store', - 'report-products' => 'WC_Admin_Reports_Products_Data_Store', - 'report-variations' => 'WC_Admin_Reports_Variations_Data_Store', - 'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store', - 'report-categories' => 'WC_Admin_Reports_Categories_Data_Store', - 'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store', - 'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store', - 'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store', - 'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store', - 'report-downloads' => 'WC_Admin_Reports_Downloads_Data_Store', + 'report-revenue-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store', + 'report-orders' => 'WC_Admin_Reports_Orders_Data_Store', + 'report-orders-stats' => 'WC_Admin_Reports_Orders_Stats_Data_Store', + 'report-products' => 'WC_Admin_Reports_Products_Data_Store', + 'report-variations' => 'WC_Admin_Reports_Variations_Data_Store', + 'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store', + 'report-categories' => 'WC_Admin_Reports_Categories_Data_Store', + 'report-taxes' => 'WC_Admin_Reports_Taxes_Data_Store', + 'report-taxes-stats' => 'WC_Admin_Reports_Taxes_Stats_Data_Store', + 'report-coupons' => 'WC_Admin_Reports_Coupons_Data_Store', + 'report-coupons-stats' => 'WC_Admin_Reports_Coupons_Stats_Data_Store', + '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', diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-order-stats-background-process.php b/plugins/woocommerce-admin/includes/class-wc-admin-order-stats-background-process.php index c7daf2e8e4a..46f76aef209 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-order-stats-background-process.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-order-stats-background-process.php @@ -71,7 +71,7 @@ class WC_Admin_Order_Stats_Background_Process extends WC_Background_Process { return false; } - WC_Admin_Reports_Orders_Data_Store::update( $order ); + WC_Admin_Reports_Orders_Stats_Data_Store::update( $order ); return false; } } diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-query.php new file mode 100644 index 00000000000..3da3be880ab --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-query.php @@ -0,0 +1,42 @@ + '2018-07-19 00:00:00', + * 'after' => '2018-07-05 00:00:00', + * 'interval' => 'week', + * 'products' => array(15, 18), + * 'coupons' => array(138), + * 'status_is' => array('completed'), + * 'status_is_not' => array('failed'), + * 'new_customers' => false, + * ); + * $report = new WC_Admin_Reports_Orders_Query( $args ); + * $mydata = $report->get_data(); + * + * @package WooCommerce Admin/Classes + */ + +defined( 'ABSPATH' ) || exit; + +/** + * WC_Admin_Reports_Orders_Query + */ +class WC_Admin_Reports_Orders_Query extends WC_Admin_Reports_Query { + + /** + * Get order data based on the current query vars. + * + * @return array + */ + public function get_data() { + $args = apply_filters( 'woocommerce_reports_orders_query_args', $this->get_query_vars() ); + + $data_store = WC_Data_Store::load( 'report-orders' ); + $results = $data_store->get_data( $args ); + return apply_filters( 'woocommerce_reports_orders_select_query', $results, $args ); + } + +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-query.php index 6d800a66dbf..6d3c8ff838d 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-query.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-query.php @@ -1,6 +1,6 @@ 'intval', - 'num_items_sold' => 'intval', - 'gross_revenue' => 'floatval', - 'coupons' => 'floatval', - 'refunds' => 'floatval', - 'taxes' => 'floatval', - 'shipping' => 'floatval', - 'net_revenue' => 'floatval', - 'avg_items_per_order' => 'floatval', - 'avg_order_value' => 'floatval', - 'num_returning_customers' => 'intval', - 'num_new_customers' => 'intval', - 'products' => 'intval', + 'order_id' => 'intval', + 'date_created' => 'strval', + 'status' => 'strval', + 'customer_id' => 'intval', + 'net_total' => 'floatval', + 'num_items_sold' => 'intval', + 'customer_type' => 'strval', ); /** - * SQL definition for each column. + * SQL columns to select in the db query and their mapping to SQL code. * * @var array */ protected $report_columns = array( - 'orders_count' => 'COUNT(*) as orders_count', - 'num_items_sold' => 'SUM(num_items_sold) as num_items_sold', - 'gross_revenue' => 'SUM(gross_total) AS gross_revenue', - 'coupons' => 'SUM(coupon_total) AS coupons', - 'refunds' => 'SUM(refund_total) AS refunds', - 'taxes' => 'SUM(tax_total) AS taxes', - 'shipping' => 'SUM(shipping_total) AS shipping', - 'net_revenue' => '( SUM(net_total) - SUM(refund_total) ) AS net_revenue', - 'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order', - 'avg_order_value' => '( SUM(net_total) - SUM(refund_total) ) / COUNT(*) AS avg_order_value', - 'num_returning_customers' => 'SUM(returning_customer = 1) AS num_returning_customers', - 'num_new_customers' => 'SUM(returning_customer = 0) AS num_new_customers', + 'order_id' => 'order_id', + 'date_created' => 'date_created', + 'status' => 'status', + 'customer_id' => 'customer_id', + 'net_total' => 'net_total', + 'num_items_sold' => 'num_items_sold', + 'customer_type' => '(CASE WHEN returning_customer <> 0 THEN "returning" ELSE "new" END) as customer_type', ); /** - * Background process to populate order stats. - * - * @var WC_Admin_Order_Stats_Background_Process - */ - protected static $background_process; - - /** - * Constructor. + * Constructor */ public function __construct() { - if ( ! self::$background_process ) { - self::$background_process = new WC_Admin_Order_Stats_Background_Process(); - } - } - - /** - * Set up all the hooks for maintaining and populating table data. - */ - public static function init() { - add_action( 'save_post', array( __CLASS__, 'sync_order' ) ); - // TODO: this is required as order update skips save_post. - add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) ); - add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) ); - add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 ); - - if ( ! self::$background_process ) { - self::$background_process = new WC_Admin_Order_Stats_Background_Process(); - } - } - - /** - * Updates the totals and intervals database queries with parameters used for Orders report: categories, coupons and order status. - * - * @param array $query_args Query arguments supplied by the user. - * @param array $totals_query Array of options for totals db query. - * @param array $intervals_query Array of options for intervals db query. - */ - protected function orders_stats_sql_filter( $query_args, &$totals_query, &$intervals_query ) { - // TODO: performance of all of this? global $wpdb; + $table_name = $wpdb->prefix . self::TABLE_NAME; + // Avoid ambigious columns in SQL query. + $this->report_columns['order_id'] = $table_name . '.' . $this->report_columns['order_id']; + $this->report_columns['date_created'] = $table_name . '.' . $this->report_columns['date_created']; + $this->report_columns['customer_id'] = $table_name . '.' . $this->report_columns['customer_id']; + } - $from_clause = ''; - $orders_stats_table = $wpdb->prefix . self::TABLE_NAME; - $operator = $this->get_match_operator( $query_args ); + /** + * Updates the database query with parameters used for orders report: coupons and products filters. + * + * @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_stats_lookup_table = $wpdb->prefix . self::TABLE_NAME; - $where_filters = array(); + $sql_query_params = $this->get_time_period_sql_params( $query_args, $order_stats_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 ) ); - // TODO: maybe move the sql inside the get_included/excluded functions? - // Products filters. - $included_products = $this->get_included_products( $query_args ); - $excluded_products = $this->get_excluded_products( $query_args ); - if ( $included_products ) { - $where_filters[] = " {$orders_stats_table}.order_id IN ( - SELECT - DISTINCT {$wpdb->prefix}wc_order_product_lookup.order_id - FROM - {$wpdb->prefix}wc_order_product_lookup - WHERE - {$wpdb->prefix}wc_order_product_lookup.product_id IN ({$included_products}) - )"; + $status_subquery = $this->get_status_subquery( $query_args ); + if ( $status_subquery ) { + $sql_query_params['where_clause'] .= " AND {$status_subquery}"; } - if ( $excluded_products ) { - $where_filters[] = " {$orders_stats_table}.order_id NOT IN ( - SELECT - DISTINCT {$wpdb->prefix}wc_order_product_lookup.order_id - FROM - {$wpdb->prefix}wc_order_product_lookup - WHERE - {$wpdb->prefix}wc_order_product_lookup.product_id IN ({$excluded_products}) - )"; + if ( $query_args['customer_type'] ) { + $returning_customer = 'returning' === $query_args['customer_type'] ? 1 : 0; + $sql_query_params['where_clause'] .= " AND returning_customer = ${returning_customer}"; } - // Coupons filters. - $included_coupons = $this->get_included_coupons( $query_args ); - $excluded_coupons = $this->get_excluded_coupons( $query_args ); + $included_coupons = $this->get_included_coupons( $query_args ); + $excluded_coupons = $this->get_excluded_coupons( $query_args ); + $order_coupon_lookup_table = $wpdb->prefix . 'wc_order_coupon_lookup'; + if ( $included_coupons || $excluded_coupons ) { + $sql_query_params['from_clause'] .= " JOIN {$order_coupon_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_coupon_lookup_table}.order_id"; + } if ( $included_coupons ) { - $where_filters[] = " {$orders_stats_table}.order_id IN ( - SELECT - DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id - FROM - {$wpdb->prefix}wc_order_coupon_lookup - WHERE - {$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$included_coupons}) - )"; + $sql_query_params['where_clause'] .= " AND {$order_coupon_lookup_table}.coupon_id IN ({$included_coupons})"; } - if ( $excluded_coupons ) { - $where_filters[] = " {$orders_stats_table}.order_id NOT IN ( - SELECT - DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id - FROM - {$wpdb->prefix}wc_order_coupon_lookup - WHERE - {$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$excluded_coupons}) - )"; + $sql_query_params['where_clause'] .= " AND {$order_coupon_lookup_table}.coupon_id NOT IN ({$excluded_coupons})"; } - $order_status_filter = $this->get_status_subquery( $query_args, $operator ); - if ( $order_status_filter ) { - $where_filters[] = $order_status_filter; + $included_products = $this->get_included_products( $query_args ); + $excluded_products = $this->get_excluded_products( $query_args ); + $order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup'; + if ( $included_products || $excluded_products ) { + $sql_query_params['from_clause'] .= " JOIN {$order_product_lookup_table} ON {$order_stats_lookup_table}.order_id = {$order_product_lookup_table}.order_id"; + } + if ( $included_products ) { + $sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})"; + } + if ( $excluded_products ) { + $sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id NOT IN ({$excluded_products})"; } - $customer_filter = $this->get_customer_subquery( $query_args ); - if ( $customer_filter ) { - $where_filters[] = $customer_filter; - } - - $where_subclause = implode( " $operator ", $where_filters ); - - // To avoid requesting the subqueries twice, the result is applied to all queries passed to the method. - if ( $where_subclause ) { - $totals_query['where_clause'] .= " AND ( $where_subclause )"; - $totals_query['from_clause'] .= $from_clause; - $intervals_query['where_clause'] .= " AND ( $where_subclause )"; - $intervals_query['from_clause'] .= $from_clause; - } + return $sql_query_params; } /** * Returns the report data based on parameters supplied by the user. * - * @since 3.5.0 * @param array $query_args Query parameters. * @return stdClass|WP_Error Data. */ @@ -205,26 +127,22 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $now = time(); $week_back = $now - WEEK_IN_SECONDS; - // These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc). + // 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', + 'orderby' => 'date_created', 'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ), 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), - 'interval' => 'week', 'fields' => '*', - - 'match' => 'all', - 'status_is' => array(), - 'status_is_not' => array(), 'product_includes' => array(), 'product_excludes' => array(), 'coupon_includes' => array(), 'coupon_excludes' => array(), - 'customer' => '', - 'categories' => array(), + 'customer_type' => null, + 'status_is' => parent::get_report_order_statuses(), + 'extended_info' => false, ); $query_args = wp_parse_args( $query_args, $defaults ); @@ -233,110 +151,67 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp if ( false === $data ) { $data = (object) array( - 'totals' => (object) array(), - 'intervals' => (object) array(), - 'total' => 0, - 'pages' => 0, - 'page_no' => 0, + 'data' => array(), + 'total' => 0, + 'pages' => 0, + 'page_no' => 0, ); - $selections = $this->selected_columns( $query_args ); - $totals_query = $this->get_time_period_sql_params( $query_args, $table_name ); - $intervals_query = $this->get_intervals_sql_params( $query_args, $table_name ); + $selections = $this->selected_columns( $query_args ); + $sql_query_params = $this->get_sql_query_params( $query_args ); - // Additional filtering for Orders report. - $this->orders_stats_sql_filter( $query_args, $totals_query, $intervals_query ); - - $totals = $wpdb->get_results( - "SELECT - {$selections} - FROM - {$table_name} - {$totals_query['from_clause']} - WHERE - 1=1 - {$totals_query['where_time_clause']} - {$totals_query['where_clause']}", - ARRAY_A + $db_records_count = (int) $wpdb->get_var( + "SELECT COUNT(*) FROM ( + SELECT + {$table_name}.order_id + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + 1=1 + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + ) AS tt" ); // WPCS: cache ok, DB call ok, unprepared SQL ok. - if ( null === $totals ) { - return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); - } - - $unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] ); - $totals[0]['products'] = $unique_products; - - $totals = (object) $this->cast_numbers( $totals[0] ); - - $db_intervals = $wpdb->get_col( - "SELECT - {$intervals_query['select_clause']} AS time_interval - FROM - {$table_name} - {$intervals_query['from_clause']} - WHERE - 1=1 - {$intervals_query['where_time_clause']} - {$intervals_query['where_clause']} - GROUP BY - time_interval" - ); // WPCS: cache ok, DB call ok, , unprepared SQL ok. - - $db_interval_count = count( $db_intervals ); - $expected_interval_count = WC_Admin_Reports_Interval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] ); - $total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] ); + $total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] ); if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { return $data; } - $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count ); - - if ( '' !== $selections ) { - $selections = ', ' . $selections; - } - - $intervals = $wpdb->get_results( + $orders_data = $wpdb->get_results( "SELECT - MAX(date_created) AS datetime_anchor, - {$intervals_query['select_clause']} AS time_interval - {$selections} - FROM - {$table_name} - {$intervals_query['from_clause']} - WHERE - 1=1 - {$intervals_query['where_time_clause']} - {$intervals_query['where_clause']} - GROUP BY - time_interval - ORDER BY - {$intervals_query['order_by_clause']} - {$intervals_query['limit']}", + {$selections} + FROM + {$table_name} + {$sql_query_params['from_clause']} + WHERE + 1=1 + {$sql_query_params['where_time_clause']} + {$sql_query_params['where_clause']} + 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 === $intervals ) { - return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); + if ( null === $orders_data ) { + return $data; } - $data = (object) array( - 'totals' => $totals, - 'intervals' => $intervals, - 'total' => $expected_interval_count, - 'pages' => $total_pages, - 'page_no' => (int) $query_args['page'], + if ( $query_args['extended_info'] ) { + $this->include_extended_info( $orders_data, $query_args ); + } + + $orders_data = array_map( array( $this, 'cast_numbers' ), $orders_data ); + $data = (object) array( + 'data' => $orders_data, + 'total' => $db_records_count, + 'pages' => $total_pages, + 'page_no' => (int) $query_args['page'], ); - if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) { - $this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data ); - $this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] ); - $this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] ); - } else { - $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); - } - $this->create_interval_subtotals( $data->intervals ); - wp_cache_set( $cache_key, $data, $this->cache_group ); } @@ -344,211 +219,151 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } /** - * Get unique products based on user time query + * Normalizes order_by clause to match to SQL query. * - * @param string $from_clause From clause with date query. - * @param string $where_time_clause Where clause with date query. - * @param string $where_clause Where clause with date query. - * @return integer Unique product count. + * @param string $order_by Order by option requeste by user. + * @return string */ - public function get_unique_product_count( $from_clause, $where_time_clause, $where_clause ) { - global $wpdb; + protected function normalize_order_by( $order_by ) { + if ( 'date' === $order_by ) { + return 'date_created'; + } - $table_name = $wpdb->prefix . self::TABLE_NAME; - - return $wpdb->get_var( - "SELECT - COUNT( DISTINCT {$wpdb->prefix}wc_order_product_lookup.product_id ) - FROM - {$wpdb->prefix}wc_order_product_lookup JOIN {$table_name} ON {$wpdb->prefix}wc_order_product_lookup.order_id = {$table_name}.order_id - {$from_clause} - WHERE - 1=1 - {$where_time_clause} - {$where_clause}" - ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + return $order_by; } /** - * Queue a background process that will repopulate the entire orders stats database. + * Returns order status subquery to be used in WHERE SQL query, based on query arguments from the user. * - * @todo Make this work on large DBs. + * @param array $query_args Parameters supplied by the user. + * @param string $operator AND or OR, based on match query argument. + * @return string */ - public static function queue_order_stats_repopulate_database() { - - // This needs to be updated to work in batches instead of getting all orders, as - // that will not work well on DBs with more than a few hundred orders. - $order_ids = wc_get_orders( - array( - 'limit' => -1, - 'status' => parent::get_report_order_statuses(), - 'type' => 'shop_order', - 'return' => 'ids', - ) - ); - - foreach ( $order_ids as $id ) { - self::$background_process->push_to_queue( $id ); + protected function get_status_subquery( $query_args, $operator = 'AND' ) { + $subqueries = array(); + if ( isset( $query_args['status_is'] ) && is_array( $query_args['status_is'] ) && count( $query_args['status_is'] ) > 0 ) { + $allowed_statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['status_is'] ); + if ( $allowed_statuses ) { + $subqueries[] = "status IN ( '" . implode( "','", $allowed_statuses ) . "' )"; + } } - self::$background_process->save(); - self::$background_process->dispatch(); + if ( isset( $query_args['status_is_not'] ) && is_array( $query_args['status_is_not'] ) && count( $query_args['status_is_not'] ) > 0 ) { + $forbidden_statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['status_is_not'] ); + if ( $forbidden_statuses ) { + $subqueries[] = "status NOT IN ( '" . implode( "','", $forbidden_statuses ) . "' )"; + } + } + + return implode( " $operator ", $subqueries ); } /** - * Add order information to the lookup table when orders are created or modified. + * Enriches the order data. * - * @param int $post_id Post ID. + * @param array $orders_data Orders data. + * @param array $query_args Query parameters. */ - public static function sync_order( $post_id ) { - if ( 'shop_order' !== get_post_type( $post_id ) ) { - return; - } + protected function include_extended_info( &$orders_data, $query_args ) { + $mapped_orders = $this->map_array_by_key( $orders_data, 'order_id' ); + $products = $this->get_products_by_order_ids( array_keys( $mapped_orders ) ); + $mapped_products = $this->map_array_by_key( $products, 'product_id' ); + $product_categories = $this->get_product_categories_by_product_ids( array_keys( $mapped_products ) ); - $order = wc_get_order( $post_id ); - if ( ! $order ) { - return; - } + $mapped_data = array(); + foreach ( $products as $product ) { + if ( ! isset( $mapped_data[ $product['order_id'] ] ) ) { + $mapped_data[ $product['order_id'] ]['products'] = array(); + $mapped_data[ $product['order_id'] ]['categories'] = array(); + } - self::update( $order ); - } - - /** - * Syncs order information when a refund is deleted. - * - * @param int $refund_id Refund ID. - * @param int $order_id Order ID. - */ - public static function sync_on_refund_delete( $refund_id, $order_id ) { - self::sync_order( $order_id ); - } - - /** - * Update the database with stats data. - * - * @param WC_Order $order Order to update row for. - * @return int|bool|null Number or rows modified or false on failure. - */ - public static function update( $order ) { - global $wpdb; - $table_name = $wpdb->prefix . self::TABLE_NAME; - - if ( ! $order->get_id() || ! $order->get_date_created() ) { - return false; - } - - if ( ! in_array( $order->get_status(), parent::get_report_order_statuses(), true ) ) { - $wpdb->delete( - $table_name, - array( - 'order_id' => $order->get_id(), + $mapped_data[ $product['order_id'] ]['products'][] = array( + 'id' => $product['product_id'], + 'name' => $product['product_name'], + ); + $mapped_data[ $product['order_id'] ]['categories'] = array_unique( + array_merge( + $mapped_data[ $product['order_id'] ]['categories'], + $product_categories[ $product['product_id'] ] ) ); - return; } - $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 ), - 'gross_total' => $order->get_total(), - 'coupon_total' => $order->get_total_discount(), - 'refund_total' => $order->get_total_refunded(), - 'tax_total' => $order->get_total_tax(), - 'shipping_total' => $order->get_shipping_total(), - 'net_total' => (float) $order->get_total() - (float) $order->get_total_tax() - (float) $order->get_shipping_total(), - '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'; - } + foreach ( $orders_data as $key => $order_data ) { + $orders_data[ $key ]['extended_info'] = $mapped_data[ $order_data['order_id'] ]; } - - // Update or add the information to the DB. - return $wpdb->replace( $table_name, $data, $format ); } /** - * Calculation methods. - */ - - /** - * Get number of items sold among all orders. + * Returns the same array index by a given key * - * @param array $order WC_Order object. - * @return int + * @param array $array Array to be looped over. + * @param string $key Key of values used for new array. + * @return array */ - protected static function get_num_items_sold( $order ) { - $num_items = 0; - - $line_items = $order->get_items( 'line_item' ); - foreach ( $line_items as $line_item ) { - $num_items += $line_item->get_quantity(); + protected function map_array_by_key( $array, $key ) { + $mapped = array(); + foreach ( $array as $item ) { + $mapped[ $item[ $key ] ] = $item; } - - return $num_items; + return $mapped; } /** - * Check to see if an order's customer has made previous orders or not + * Get product Ids, names, and categories from order IDs. * - * @param array $order WC_Order object. - * @return bool + * @param array $order_ids Array of order IDs. + * @return array */ - protected static function is_returning_customer( $order ) { - $customer_id = $order->get_user_id(); + protected function get_products_by_order_ids( $order_ids ) { + global $wpdb; + $order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup'; + $included_order_ids = implode( ',', $order_ids ); - if ( 0 === $customer_id ) { - return false; + $products = $wpdb->get_results( + "SELECT order_id, ID as product_id, post_title as product_name + FROM {$wpdb->prefix}posts + JOIN {$order_product_lookup_table} ON {$order_product_lookup_table}.product_id = {$wpdb->prefix}posts.ID + WHERE + order_id IN ({$included_order_ids}) + ", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + return $products; + } + + /** + * Get product categories by array of product IDs + * + * @param array $product_ids Product IDs. + * @return array + */ + protected function get_product_categories_by_product_ids( $product_ids ) { + global $wpdb; + $order_product_lookup_table = $wpdb->prefix . 'wc_order_product_lookup'; + $included_product_ids = implode( ',', $product_ids ); + + $product_categories = $wpdb->get_results( + "SELECT term_id AS category_id, object_id AS product_id + FROM {$wpdb->prefix}term_relationships + JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id + WHERE + object_id IN (${included_product_ids}) + AND taxonomy = 'product_cat' + ", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $mapped_product_categories = array(); + foreach ( $product_categories as $category ) { + $mapped_product_categories[ $category['product_id'] ][] = $category['category_id']; } - $customer_orders = get_posts( - array( - 'meta_key' => '_customer_user', // WPCS: slow query ok. - 'meta_value' => $customer_id, // WPCS: slow query ok. - 'post_type' => 'shop_order', - 'post_status' => array( 'wc-on-hold', 'wc-processing', 'wc-completed' ), - 'numberposts' => 2, - ) - ); - - return count( $customer_orders ) > 1; + return $mapped_product_categories; } + /** * Returns string to be used as cache key for the data. * @@ -558,4 +373,5 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp protected function get_cache_key( $params ) { return 'woocommerce_' . self::TABLE_NAME . '_' . md5( wp_json_encode( $params ) ); } + } diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php new file mode 100644 index 00000000000..be25ee82596 --- /dev/null +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php @@ -0,0 +1,561 @@ + 'intval', + 'num_items_sold' => 'intval', + 'gross_revenue' => 'floatval', + 'coupons' => 'floatval', + 'refunds' => 'floatval', + 'taxes' => 'floatval', + 'shipping' => 'floatval', + 'net_revenue' => 'floatval', + 'avg_items_per_order' => 'floatval', + 'avg_order_value' => 'floatval', + 'num_returning_customers' => 'intval', + 'num_new_customers' => 'intval', + 'products' => 'intval', + ); + + /** + * SQL definition for each column. + * + * @var array + */ + protected $report_columns = array( + 'orders_count' => 'COUNT(*) as orders_count', + 'num_items_sold' => 'SUM(num_items_sold) as num_items_sold', + 'gross_revenue' => 'SUM(gross_total) AS gross_revenue', + 'coupons' => 'SUM(coupon_total) AS coupons', + 'refunds' => 'SUM(refund_total) AS refunds', + 'taxes' => 'SUM(tax_total) AS taxes', + 'shipping' => 'SUM(shipping_total) AS shipping', + 'net_revenue' => '( SUM(net_total) - SUM(refund_total) ) AS net_revenue', + 'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order', + 'avg_order_value' => '( SUM(net_total) - SUM(refund_total) ) / COUNT(*) AS avg_order_value', + 'num_returning_customers' => 'SUM(returning_customer = 1) AS num_returning_customers', + 'num_new_customers' => 'SUM(returning_customer = 0) AS num_new_customers', + ); + + /** + * Background process to populate order stats. + * + * @var WC_Admin_Order_Stats_Background_Process + */ + protected static $background_process; + + /** + * Constructor. + */ + public function __construct() { + if ( ! self::$background_process ) { + self::$background_process = new WC_Admin_Order_Stats_Background_Process(); + } + } + + /** + * Set up all the hooks for maintaining and populating table data. + */ + public static function init() { + add_action( 'save_post', array( __CLASS__, 'sync_order' ) ); + // TODO: this is required as order update skips save_post. + add_action( 'clean_post_cache', array( __CLASS__, 'sync_order' ) ); + add_action( 'woocommerce_order_refunded', array( __CLASS__, 'sync_order' ) ); + add_action( 'woocommerce_refund_deleted', array( __CLASS__, 'sync_on_refund_delete' ), 10, 2 ); + + if ( ! self::$background_process ) { + self::$background_process = new WC_Admin_Order_Stats_Background_Process(); + } + } + + /** + * Updates the totals and intervals database queries with parameters used for Orders report: categories, coupons and order status. + * + * @param array $query_args Query arguments supplied by the user. + * @param array $totals_query Array of options for totals db query. + * @param array $intervals_query Array of options for intervals db query. + */ + protected function orders_stats_sql_filter( $query_args, &$totals_query, &$intervals_query ) { + // TODO: performance of all of this? + global $wpdb; + + $from_clause = ''; + $orders_stats_table = $wpdb->prefix . self::TABLE_NAME; + $operator = $this->get_match_operator( $query_args ); + + $where_filters = array(); + + // TODO: maybe move the sql inside the get_included/excluded functions? + // Products filters. + $included_products = $this->get_included_products( $query_args ); + $excluded_products = $this->get_excluded_products( $query_args ); + if ( $included_products ) { + $where_filters[] = " {$orders_stats_table}.order_id IN ( + SELECT + DISTINCT {$wpdb->prefix}wc_order_product_lookup.order_id + FROM + {$wpdb->prefix}wc_order_product_lookup + WHERE + {$wpdb->prefix}wc_order_product_lookup.product_id IN ({$included_products}) + )"; + } + + if ( $excluded_products ) { + $where_filters[] = " {$orders_stats_table}.order_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}wc_order_product_lookup.order_id + FROM + {$wpdb->prefix}wc_order_product_lookup + WHERE + {$wpdb->prefix}wc_order_product_lookup.product_id IN ({$excluded_products}) + )"; + } + + // Coupons filters. + $included_coupons = $this->get_included_coupons( $query_args ); + $excluded_coupons = $this->get_excluded_coupons( $query_args ); + if ( $included_coupons ) { + $where_filters[] = " {$orders_stats_table}.order_id IN ( + SELECT + DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id + FROM + {$wpdb->prefix}wc_order_coupon_lookup + WHERE + {$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$included_coupons}) + )"; + } + + if ( $excluded_coupons ) { + $where_filters[] = " {$orders_stats_table}.order_id NOT IN ( + SELECT + DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id + FROM + {$wpdb->prefix}wc_order_coupon_lookup + WHERE + {$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$excluded_coupons}) + )"; + } + + $order_status_filter = $this->get_status_subquery( $query_args, $operator ); + if ( $order_status_filter ) { + $where_filters[] = $order_status_filter; + } + + $customer_filter = $this->get_customer_subquery( $query_args ); + if ( $customer_filter ) { + $where_filters[] = $customer_filter; + } + + $where_subclause = implode( " $operator ", $where_filters ); + + // To avoid requesting the subqueries twice, the result is applied to all queries passed to the method. + if ( $where_subclause ) { + $totals_query['where_clause'] .= " AND ( $where_subclause )"; + $totals_query['from_clause'] .= $from_clause; + $intervals_query['where_clause'] .= " AND ( $where_subclause )"; + $intervals_query['from_clause'] .= $from_clause; + } + } + + /** + * Returns the report data based on parameters supplied by the user. + * + * @since 3.5.0 + * @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; + $now = time(); + $week_back = $now - WEEK_IN_SECONDS; + + // These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc). + $defaults = array( + 'per_page' => get_option( 'posts_per_page' ), + 'page' => 1, + 'order' => 'DESC', + 'orderby' => 'date', + 'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ), + 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), + 'interval' => 'week', + 'fields' => '*', + + 'match' => 'all', + 'status_is' => array(), + 'status_is_not' => array(), + 'product_includes' => array(), + 'product_excludes' => array(), + 'coupon_includes' => array(), + 'coupon_excludes' => array(), + 'customer' => '', + 'categories' => array(), + ); + $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( + 'totals' => (object) array(), + 'intervals' => (object) array(), + 'total' => 0, + 'pages' => 0, + 'page_no' => 0, + ); + + $selections = $this->selected_columns( $query_args ); + $totals_query = $this->get_time_period_sql_params( $query_args, $table_name ); + $intervals_query = $this->get_intervals_sql_params( $query_args, $table_name ); + + // Additional filtering for Orders report. + $this->orders_stats_sql_filter( $query_args, $totals_query, $intervals_query ); + + $totals = $wpdb->get_results( + "SELECT + {$selections} + FROM + {$table_name} + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']}", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + if ( null === $totals ) { + return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); + } + + $unique_products = $this->get_unique_product_count( $totals_query['from_clause'], $totals_query['where_time_clause'], $totals_query['where_clause'] ); + $totals[0]['products'] = $unique_products; + + $totals = (object) $this->cast_numbers( $totals[0] ); + + $db_intervals = $wpdb->get_col( + "SELECT + {$intervals_query['select_clause']} AS time_interval + FROM + {$table_name} + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + GROUP BY + time_interval" + ); // WPCS: cache ok, DB call ok, , unprepared SQL ok. + + $db_interval_count = count( $db_intervals ); + $expected_interval_count = WC_Admin_Reports_Interval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] ); + $total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] ); + + if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) { + return $data; + } + + $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count ); + + if ( '' !== $selections ) { + $selections = ', ' . $selections; + } + + $intervals = $wpdb->get_results( + "SELECT + MAX(date_created) AS datetime_anchor, + {$intervals_query['select_clause']} AS time_interval + {$selections} + FROM + {$table_name} + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + GROUP BY + time_interval + ORDER BY + {$intervals_query['order_by_clause']} + {$intervals_query['limit']}", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + if ( null === $intervals ) { + return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); + } + + $data = (object) array( + 'totals' => $totals, + 'intervals' => $intervals, + 'total' => $expected_interval_count, + 'pages' => $total_pages, + 'page_no' => (int) $query_args['page'], + ); + + if ( WC_Admin_Reports_Interval::intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) { + $this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data ); + $this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] ); + $this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] ); + } else { + $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); + } + $this->create_interval_subtotals( $data->intervals ); + + wp_cache_set( $cache_key, $data, $this->cache_group ); + } + + return $data; + } + + /** + * Get unique products based on user time query + * + * @param string $from_clause From clause with date query. + * @param string $where_time_clause Where clause with date query. + * @param string $where_clause Where clause with date query. + * @return integer Unique product count. + */ + public function get_unique_product_count( $from_clause, $where_time_clause, $where_clause ) { + global $wpdb; + + $table_name = $wpdb->prefix . self::TABLE_NAME; + + return $wpdb->get_var( + "SELECT + COUNT( DISTINCT {$wpdb->prefix}wc_order_product_lookup.product_id ) + FROM + {$wpdb->prefix}wc_order_product_lookup JOIN {$table_name} ON {$wpdb->prefix}wc_order_product_lookup.order_id = {$table_name}.order_id + {$from_clause} + WHERE + 1=1 + {$where_time_clause} + {$where_clause}" + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + } + + /** + * Queue a background process that will repopulate the entire orders stats database. + * + * @todo Make this work on large DBs. + */ + public static function queue_order_stats_repopulate_database() { + + // This needs to be updated to work in batches instead of getting all orders, as + // that will not work well on DBs with more than a few hundred orders. + $order_ids = wc_get_orders( + array( + 'limit' => -1, + 'status' => parent::get_report_order_statuses(), + 'type' => 'shop_order', + 'return' => 'ids', + ) + ); + + foreach ( $order_ids as $id ) { + self::$background_process->push_to_queue( $id ); + } + + self::$background_process->save(); + self::$background_process->dispatch(); + } + + /** + * Add order 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; + } + + self::update( $order ); + } + + /** + * Syncs order information when a refund is deleted. + * + * @param int $refund_id Refund ID. + * @param int $order_id Order ID. + */ + public static function sync_on_refund_delete( $refund_id, $order_id ) { + self::sync_order( $order_id ); + } + + /** + * Update the database with stats data. + * + * @param WC_Order $order Order to update row for. + * @return int|bool|null Number or rows modified or false on failure. + */ + public static function update( $order ) { + global $wpdb; + $table_name = $wpdb->prefix . self::TABLE_NAME; + + if ( ! $order->get_id() || ! $order->get_date_created() ) { + return false; + } + + if ( ! in_array( $order->get_status(), parent::get_report_order_statuses(), true ) ) { + $wpdb->delete( + $table_name, + array( + 'order_id' => $order->get_id(), + ) + ); + return; + } + + $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 ), + 'gross_total' => $order->get_total(), + 'coupon_total' => $order->get_total_discount(), + 'refund_total' => $order->get_total_refunded(), + 'tax_total' => $order->get_total_tax(), + 'shipping_total' => $order->get_shipping_total(), + 'net_total' => (float) $order->get_total() - (float) $order->get_total_tax() - (float) $order->get_shipping_total(), + '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, $format ); + } + + /** + * Calculation methods. + */ + + /** + * Get number of items sold among all orders. + * + * @param array $order WC_Order object. + * @return int + */ + protected static function get_num_items_sold( $order ) { + $num_items = 0; + + $line_items = $order->get_items( 'line_item' ); + foreach ( $line_items as $line_item ) { + $num_items += $line_item->get_quantity(); + } + + return $num_items; + } + + /** + * Check to see if an order's customer has made previous orders or not + * + * @param array $order WC_Order object. + * @return bool + */ + protected static function is_returning_customer( $order ) { + $customer_id = $order->get_user_id(); + + if ( 0 === $customer_id ) { + return false; + } + + $customer_orders = get_posts( + array( + 'meta_key' => '_customer_user', // WPCS: slow query ok. + 'meta_value' => $customer_id, // WPCS: slow query ok. + 'post_type' => 'shop_order', + 'post_status' => array( 'wc-on-hold', 'wc-processing', 'wc-completed' ), + 'numberposts' => 2, + ) + ); + + return count( $customer_orders ) > 1; + } + + /** + * 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 . '_stats_' . md5( wp_json_encode( $params ) ); + } +} diff --git a/plugins/woocommerce-admin/tests/api/reports-orders.php b/plugins/woocommerce-admin/tests/api/reports-orders.php new file mode 100644 index 00000000000..6e1e798b174 --- /dev/null +++ b/plugins/woocommerce-admin/tests/api/reports-orders.php @@ -0,0 +1,122 @@ +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 getting reports. + * + * @since 3.5.0 + */ + public function test_get_reports() { + wp_set_current_user( $this->user ); + WC_Helper_Reports::reset_stats_dbs(); + + // Populate all of the data. + $product = new WC_Product_Simple(); + $product->set_name( 'Test Product' ); + $product->set_regular_price( 25 ); + $product->save(); + + $order = WC_Helper_Order::create_order( 1, $product ); + $order->set_status( 'completed' ); + $order->set_total( 100 ); // $25 x 4. + $order->save(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) ); + $reports = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $reports ) ); + + $order_report = reset( $reports ); + + $this->assertEquals( $order->get_id(), $order_report['order_id'] ); + $this->assertEquals( date( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ), $order_report['date_created'] ); + $this->assertEquals( 0, $order_report['customer_id'] ); // @TODO: This should be 1, but customer_id is returning 0 in lookup table. + $this->assertEquals( 4, $order_report['num_items_sold'] ); + $this->assertEquals( 90.0, $order_report['net_total'] ); // 25 x 4 - 10 (shipping) + $this->assertEquals( 'new', $order_report['customer_type'] ); + $this->assertArrayHasKey( '_links', $order_report ); + $this->assertArrayHasKey( 'order', $order_report['_links'] ); + } + + /** + * 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 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( 8, count( $properties ) ); + $this->assertArrayHasKey( 'order_id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'customer_id', $properties ); + $this->assertArrayHasKey( 'net_total', $properties ); + $this->assertArrayHasKey( 'num_items_sold', $properties ); + $this->assertArrayHasKey( 'customer_type', $properties ); + $this->assertArrayHasKey( 'extended_info', $properties ); + } +} 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 86d93727db1..9bc4bcb43c7 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 @@ -17,7 +17,7 @@ class WC_Helper_Reports { */ public static function reset_stats_dbs() { global $wpdb; - $wpdb->query( "DELETE FROM $wpdb->prefix" . WC_Admin_Reports_Orders_Data_Store::TABLE_NAME ); // @codingStandardsIgnoreLine. + $wpdb->query( "DELETE FROM $wpdb->prefix" . WC_Admin_Reports_Orders_Stats_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. diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php similarity index 99% rename from plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php rename to plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php index 4709fd6284b..09a01eff2bd 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php @@ -6,9 +6,9 @@ */ /** - * Class WC_Tests_Reports_Orders + * Class WC_Tests_Reports_Orders_Stats */ -class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { +class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { /** * Test the calculations and querying works correctly for the base case of 1 order. @@ -41,7 +41,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { ) ); - $data_store = new WC_Admin_Reports_Orders_Data_Store(); + $data_store = new WC_Admin_Reports_Orders_Stats_Data_Store(); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $end_time = date( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() ); @@ -321,7 +321,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { } } - $data_store = new WC_Admin_Reports_Orders_Data_Store(); + $data_store = new WC_Admin_Reports_Orders_Stats_Data_Store(); // Tests for before & after set to current hour. $current_hour = new DateTime(); diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-revenue-stats.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-revenue-stats.php index f417d1bda92..359dd0243df 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-revenue-stats.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-revenue-stats.php @@ -38,7 +38,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { $order->save(); // /reports/revenue/stats is mapped to Orders_Data_Store. - $data_store = new WC_Admin_Reports_Orders_Data_Store(); + $data_store = new WC_Admin_Reports_Orders_Stats_Data_Store(); $start_time = date( 'Y-m-d H:00:00', $order->get_date_created()->getOffsetTimestamp() ); $end_time = date( 'Y-m-d H:59:59', $order->get_date_created()->getOffsetTimestamp() );