diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php index e395698745f..4e606822433 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-customers-controller.php @@ -17,7 +17,7 @@ defined( 'ABSPATH' ) || exit; */ class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller { - // TODO Add support for guests here. See https://wp.me/p7bje6-1dM. + // @todo Add support for guests here. See https://wp.me/p7bje6-1dM. /** * Endpoint namespace. diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php index 187beb587da..5aa963ff458 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-coupons-stats-controller.php @@ -39,15 +39,16 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con * @return array */ protected function prepare_reports_query( $request ) { - $args = array(); - $args['before'] = $request['before']; - $args['after'] = $request['after']; - $args['interval'] = $request['interval']; - $args['page'] = $request['page']; - $args['per_page'] = $request['per_page']; - $args['orderby'] = $request['orderby']; - $args['order'] = $request['order']; - $args['coupons'] = (array) $request['coupons']; + $args = array(); + $args['before'] = $request['before']; + $args['after'] = $request['after']; + $args['interval'] = $request['interval']; + $args['page'] = $request['page']; + $args['per_page'] = $request['per_page']; + $args['orderby'] = $request['orderby']; + $args['order'] = $request['order']; + $args['coupons'] = (array) $request['coupons']; + $args['segmentby'] = $request['segmentby']; return $args; } @@ -61,7 +62,11 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con public function get_items( $request ) { $query_args = $this->prepare_reports_query( $request ); $coupons_query = new WC_Admin_Reports_Coupons_Stats_Query( $query_args ); - $report_data = $coupons_query->get_data(); + try { + $report_data = $coupons_query->get_data(); + } catch ( WC_Admin_Reports_Parameter_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } $out_data = array( 'totals' => get_object_vars( $report_data->totals ), @@ -132,7 +137,7 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con * @return array */ public function get_item_schema() { - $totals = array( + $data_values = array( 'amount' => array( 'description' => __( 'Net discount amount.', 'wc-admin' ), 'type' => 'number', @@ -156,6 +161,35 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con ), ); + $segments = array( + 'segments' => array( + 'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'segment_id' => array( + 'description' => __( 'Segment identificator.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotals' => array( + 'description' => __( 'Interval subtotals.', 'wc-admin' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $data_values, + ), + ), + ), + ), + ); + + $totals = array_merge( $data_values, $segments ); + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_coupons_stats', @@ -302,6 +336,17 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con 'type' => 'integer', ), ); + $params['segmentby'] = array( + 'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ), + 'type' => 'string', + 'enum' => array( + 'product', + 'variation', + 'category', + 'coupon', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php index d500b888ff7..207edd59cf4 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php @@ -77,7 +77,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $report_data = $customers_query->get_data(); $out_data = array( 'totals' => $report_data, - // TODO: is this needed? the single element array tricks the isReportDataEmpty() selector. + // @todo: is this needed? the single element array tricks the isReportDataEmpty() selector. 'intervals' => array( (object) array() ), ); @@ -119,7 +119,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C * @return array */ public function get_item_schema() { - // TODO: should any of these be 'indicator's? + // @todo: should any of these be 'indicator's? $totals = array( 'customers_count' => array( 'description' => __( 'Number of customers.', 'wc-admin' ), @@ -161,7 +161,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'readonly' => true, 'properties' => $totals, ), - 'intervals' => array( // TODO: remove this? + 'intervals' => array( // @todo: remove this? 'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ), 'type' => 'array', 'context' => array( 'view', 'edit' ), 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 f62cf855aad..3fba1631a14 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 @@ -56,6 +56,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report $args['coupon_excludes'] = (array) $request['coupon_excludes']; $args['customer'] = $request['customer']; $args['categories'] = (array) $request['categories']; + $args['segmentby'] = $request['segmentby']; return $args; } @@ -69,7 +70,11 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report public function get_items( $request ) { $query_args = $this->prepare_reports_query( $request ); $orders_query = new WC_Admin_Reports_Orders_Stats_Query( $query_args ); - $report_data = $orders_query->get_data(); + try { + $report_data = $orders_query->get_data(); + } catch ( WC_Admin_Reports_Parameter_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } $out_data = array( 'totals' => get_object_vars( $report_data->totals ), @@ -140,15 +145,15 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report * @return array */ public function get_item_schema() { - $totals = array( - 'net_revenue' => array( + $data_values = array( + 'net_revenue' => array( 'description' => __( 'Net revenue.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, 'format' => 'currency', ), - 'orders_count' => array( + 'orders_count' => array( 'description' => __( 'Amount of orders', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -163,14 +168,78 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report 'indicator' => true, 'format' => 'currency', ), - 'avg_items_per_order' => array( + 'avg_items_per_order' => array( 'description' => __( 'Average items per order', '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, + ), + 'coupons' => array( + 'description' => __( 'Amount discounted by coupons', 'wc-admin' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'num_returning_customers' => array( + 'description' => __( 'Number of orders done by returning customers', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'num_new_customers' => array( + 'description' => __( 'Number of orders done by new customers', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'products' => array( + 'description' => __( 'Number of distinct products sold.', 'wc-admin' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), ); + $segments = array( + 'segments' => array( + 'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'segment_id' => array( + 'description' => __( 'Segment identificator.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotals' => array( + 'description' => __( 'Interval subtotals.', 'wc-admin' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $data_values, + ), + ), + ), + ), + ); + + $totals = array_merge( $data_values, $segments ); + + // Products is not shown in intervals. + unset( $data_values['products'] ); + + $intervals = array_merge( $data_values, $segments ); + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_orders_stats', @@ -227,7 +296,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, - 'properties' => $totals, + 'properties' => $intervals, ), ), ), @@ -358,7 +427,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $params['coupon_includes'] = array( + $params['coupon_includes'] = array( 'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'wc-admin' ), 'type' => 'array', 'items' => array( @@ -367,7 +436,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report 'default' => array(), 'sanitize_callback' => 'wp_parse_id_list', ); - $params['coupon_excludes'] = array( + $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( @@ -385,6 +454,18 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report ), 'validate_callback' => 'rest_validate_request_arg', ); + $params['segmentby'] = array( + 'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ), + 'type' => 'string', + 'enum' => array( + 'product', + 'category', + 'variation', + 'coupon', + 'customer_type', // new vs returning. + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php index 087b90b599a..945091bf466 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-products-stats-controller.php @@ -74,8 +74,12 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co } } - $query = new WC_Admin_Reports_Products_Stats_Query( $query_args ); - $report_data = $query->get_data(); + $query = new WC_Admin_Reports_Products_Stats_Query( $query_args ); + try { + $report_data = $query->get_data(); + } catch ( WC_Admin_Reports_Parameter_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } $out_data = array( 'totals' => get_object_vars( $report_data->totals ), @@ -146,7 +150,7 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co * @return array */ public function get_item_schema() { - $totals = array( + $data_values = array( 'items_sold' => array( 'description' => __( 'Number of items sold.', 'wc-admin' ), 'type' => 'integer', @@ -169,6 +173,35 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co ), ); + $segments = array( + 'segments' => array( + 'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'segment_id' => array( + 'description' => __( 'Segment identificator.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotals' => array( + 'description' => __( 'Interval subtotals.', 'wc-admin' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $data_values, + ), + ), + ), + ), + ); + + $totals = array_merge( $data_values, $segments ); + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_products_stats', @@ -350,6 +383,16 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co 'type' => 'integer', ), ); + $params['segmentby'] = array( + 'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ), + 'type' => 'string', + 'enum' => array( + 'product', + 'category', + 'variation', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php index 324b780c4c5..73f48a05dd9 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-revenue-stats-controller.php @@ -38,14 +38,15 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con * @return array */ protected function prepare_reports_query( $request ) { - $args = array(); - $args['before'] = $request['before']; - $args['after'] = $request['after']; - $args['interval'] = $request['interval']; - $args['page'] = $request['page']; - $args['per_page'] = $request['per_page']; - $args['orderby'] = $request['orderby']; - $args['order'] = $request['order']; + $args = array(); + $args['before'] = $request['before']; + $args['after'] = $request['after']; + $args['interval'] = $request['interval']; + $args['page'] = $request['page']; + $args['per_page'] = $request['per_page']; + $args['orderby'] = $request['orderby']; + $args['order'] = $request['order']; + $args['segmentby'] = $request['segmentby']; return $args; } @@ -59,7 +60,11 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con public function get_items( $request ) { $query_args = $this->prepare_reports_query( $request ); $reports_revenue = new WC_Admin_Reports_Revenue_Query( $query_args ); - $report_data = $reports_revenue->get_data(); + try { + $report_data = $reports_revenue->get_data(); + } catch ( WC_Admin_Reports_Parameter_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } $out_data = array( 'totals' => get_object_vars( $report_data->totals ), @@ -130,7 +135,7 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con * @return array */ public function get_item_schema() { - $totals = array( + $data_values = array( 'gross_revenue' => array( 'description' => __( 'Gross revenue.', 'wc-admin' ), 'type' => 'number', @@ -197,8 +202,39 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con ), ); - $intervals = $totals; - unset( $intervals['products'] ); + $segments = array( + 'segments' => array( + 'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'segment_id' => array( + 'description' => __( 'Segment identificator.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotals' => array( + 'description' => __( 'Interval subtotals.', 'wc-admin' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $data_values, + ), + ), + ), + ), + ); + + $totals = array_merge( $data_values, $segments ); + + // Products is not shown in intervals. + unset( $data_values['products'] ); + + $intervals = array_merge( $data_values, $segments ); $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', @@ -342,6 +378,18 @@ class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Con ), 'validate_callback' => 'rest_validate_request_arg', ); + $params['segmentby'] = array( + 'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ), + 'type' => 'string', + 'enum' => array( + 'product', + 'category', + 'variation', + 'coupon', + 'customer_type', // new vs returning. + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php index d545e468492..1ae6c0cd590 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-taxes-stats-controller.php @@ -68,15 +68,16 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr * @return array */ protected function prepare_reports_query( $request ) { - $args = array(); - $args['before'] = $request['before']; - $args['after'] = $request['after']; - $args['interval'] = $request['interval']; - $args['page'] = $request['page']; - $args['per_page'] = $request['per_page']; - $args['orderby'] = $request['orderby']; - $args['order'] = $request['order']; - $args['taxes'] = (array) $request['taxes']; + $args = array(); + $args['before'] = $request['before']; + $args['after'] = $request['after']; + $args['interval'] = $request['interval']; + $args['page'] = $request['page']; + $args['per_page'] = $request['per_page']; + $args['orderby'] = $request['orderby']; + $args['order'] = $request['order']; + $args['taxes'] = (array) $request['taxes']; + $args['segmentby'] = $request['segmentby']; return $args; } @@ -161,7 +162,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr * @return array */ public function get_item_schema() { - $totals = array( + $data_values = array( 'total_tax' => array( 'description' => __( 'Total tax.', 'wc-admin' ), 'type' => 'number', @@ -192,7 +193,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'tax_codes' => array( + 'tax_codes' => array( 'description' => __( 'Amount of tax codes.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -200,6 +201,35 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr ), ); + $segments = array( + 'segments' => array( + 'description' => __( 'Reports data grouped by segment condition.', 'wc-admin' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'segment_id' => array( + 'description' => __( 'Segment identificator.', 'wc-admin' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotals' => array( + 'description' => __( 'Interval subtotals.', 'wc-admin' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $data_values, + ), + ), + ), + ), + ); + + $totals = array_merge( $data_values, $segments ); + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_taxes_stats', @@ -273,9 +303,9 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr * @return array */ public function get_collection_params() { - $params = array(); - $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); - $params['page'] = array( + $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, @@ -283,7 +313,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr 'validate_callback' => 'rest_validate_request_arg', 'minimum' => 1, ); - $params['per_page'] = array( + $params['per_page'] = array( 'description' => __( 'Maximum number of items to be returned in result set.', 'wc-admin' ), 'type' => 'integer', 'default' => 10, @@ -292,26 +322,26 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['after'] = array( + $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( + $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( + $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( + $params['orderby'] = array( 'description' => __( 'Sort collection by object attribute.', 'wc-admin' ), 'type' => 'string', 'default' => 'date', @@ -324,7 +354,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr ), 'validate_callback' => 'rest_validate_request_arg', ); - $params['interval'] = array( + $params['interval'] = array( 'description' => __( 'Time interval to use for buckets in the returned data.', 'wc-admin' ), 'type' => 'string', 'default' => 'week', @@ -338,7 +368,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr ), 'validate_callback' => 'rest_validate_request_arg', ); - $params['taxes'] = array( + $params['taxes'] = array( 'description' => __( 'Limit result set to all items that have the specified term assigned in the taxes taxonomy.', 'wc-admin' ), 'type' => 'array', 'sanitize_callback' => 'wp_parse_id_list', @@ -347,6 +377,14 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr 'type' => 'integer', ), ); + $params['segmentby'] = array( + 'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ), + 'type' => 'string', + 'enum' => array( + 'tax_rate_id', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } 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 290ca5fbed3..470ac8dcc81 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -110,6 +110,19 @@ class WC_Admin_Api_Init { // Common date time code. require_once dirname( __FILE__ ) . '/class-wc-admin-reports-interval.php'; + // Exceptions. + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-parameter-exception.php'; + + // WC Class extensions. + require_once dirname( __FILE__ ) . '/class-wc-admin-order.php'; + + // Segmentation. + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-segmenting.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-stats-segmenting.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-stats-segmenting.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-coupons-stats-segmenting.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-taxes-stats-segmenting.php'; + // 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'; @@ -434,6 +447,9 @@ class WC_Admin_Api_Init { * Init orders data store. */ public static function orders_data_store_init() { + // Activate WC_Order extension. + WC_Admin_Order::add_filters(); + // Initialize data stores. WC_Admin_Reports_Orders_Stats_Data_Store::init(); WC_Admin_Reports_Products_Data_Store::init(); WC_Admin_Reports_Taxes_Data_Store::init(); @@ -483,7 +499,7 @@ class WC_Admin_Api_Init { $order_ids = $order_query->get_orders(); foreach ( $order_ids as $order_id ) { - // TODO: schedule single order update if this fails? + // @todo: schedule single order update if this fails? WC_Admin_Reports_Orders_Stats_Data_Store::sync_order( $order_id ); WC_Admin_Reports_Products_Data_Store::sync_order_products( $order_id ); WC_Admin_Reports_Coupons_Data_Store::sync_order_coupons( $order_id ); @@ -634,7 +650,7 @@ class WC_Admin_Api_Init { $customer_ids = $customer_query->get_results(); foreach ( $customer_ids as $customer_id ) { - // TODO: schedule single customer update if this fails? + // @todo: schedule single customer update if this fails? WC_Admin_Reports_Customers_Data_Store::update_registered_customer( $customer_id ); } } @@ -681,7 +697,7 @@ class WC_Admin_Api_Init { return array_merge( $wc_tables, array( - // TODO: will this work on multisite? + // @todo: will this work on multisite? "{$wpdb->prefix}wc_order_stats", "{$wpdb->prefix}wc_order_product_lookup", "{$wpdb->prefix}wc_order_tax_lookup", @@ -733,6 +749,12 @@ class WC_Admin_Api_Init { date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL, product_qty INT UNSIGNED NOT NULL, product_net_revenue double DEFAULT 0 NOT NULL, + product_gross_revenue double DEFAULT 0 NOT NULL, + coupon_amount double DEFAULT 0 NOT NULL, + tax_amount double DEFAULT 0 NOT NULL, + shipping_amount double DEFAULT 0 NOT NULL, + shipping_tax_amount double DEFAULT 0 NOT NULL, + refund_amount double DEFAULT 0 NOT NULL, PRIMARY KEY (order_item_id), KEY order_id (order_id), KEY product_id (product_id), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-order.php b/plugins/woocommerce-admin/includes/class-wc-admin-order.php new file mode 100644 index 00000000000..772878cbd99 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-order.php @@ -0,0 +1,189 @@ +get_item_quantity_refunded( $item ); + $product_qty = $item->get_quantity( 'edit' ) - $quantity_refunded; + + $order_items = $this->get_item_count(); + if ( 0 === $order_items ) { + return 0; + } + + $refunded = $this->get_total_shipping_refunded(); + if ( $refunded > 0 ) { + $total_shipping_amount = $this->get_shipping_total() - $refunded; + } else { + $total_shipping_amount = $this->get_shipping_total(); + } + + return $total_shipping_amount / $order_items * $product_qty; + } + + /** + * Save refund amounts and quantities for the order in an array for later use in calculations. + */ + protected function set_order_refund_items() { + if ( ! isset( $this->refunded_line_items ) ) { + $refunds = $this->get_refunds(); + $refunded_line_items = array(); + foreach ( $refunds as $refund ) { + foreach ( $refund->get_items() as $refunded_item ) { + $line_item_id = wc_get_order_item_meta( $refunded_item->get_id(), '_refunded_item_id', true ); + if ( ! isset( $refunded_line_items[ $line_item_id ] ) ) { + $refunded_line_items[ $line_item_id ]['quantity'] = 0; + $refunded_line_items[ $line_item_id ]['subtotal'] = 0; + } + $refunded_line_items[ $line_item_id ]['quantity'] += absint( $refunded_item['quantity'] ); + $refunded_line_items[ $line_item_id ]['subtotal'] += abs( $refunded_item['subtotal'] ); + } + } + $this->refunded_line_items = $refunded_line_items; + } + } + + /** + * Get quantity refunded for the line item. + * + * @param WC_Order_Item $item Line item from order. + * + * @return int + */ + public function get_item_quantity_refunded( $item ) { + $this->set_order_refund_items(); + $order_item_id = $item->get_id(); + + return isset( $this->refunded_line_items[ $order_item_id ] ) ? $this->refunded_line_items[ $order_item_id ]['quantity'] : 0; + } + + /** + * Get amount refunded for the line item. + * + * @param WC_Order_Item $item Line item from order. + * + * @return int + */ + public function get_item_amount_refunded( $item ) { + $this->set_order_refund_items(); + $order_item_id = $item->get_id(); + + return isset( $this->refunded_line_items[ $order_item_id ] ) ? $this->refunded_line_items[ $order_item_id ]['subtotal'] : 0; + } + + /** + * Get item quantity minus refunded quantity for the line item. + * + * @param WC_Order_Item $item Line item from order. + * + * @return int + */ + public function get_item_quantity_minus_refunded( $item ) { + return $item->get_quantity( 'edit' ) - $this->get_item_quantity_refunded( $item ); + } + + /** + * Calculate shipping tax amount for line item/product as a total shipping tax amount ratio based on quantity. + * + * Loosely based on code in includes/admin/meta-boxes/views/html-order-item(s).php. + * + * @todo: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? + * + * @param WC_Order_Item $item Line item from order. + * + * @return float|int + */ + public function get_item_shipping_tax_amount( $item ) { + $order_items = $this->get_item_count(); + if ( 0 === $order_items ) { + return 0; + } + + $quantity_refunded = $this->get_item_quantity_refunded( $item ); + $product_qty = $item->get_quantity( 'edit' ) - $quantity_refunded; + $order_taxes = $this->get_taxes(); + $line_items_shipping = $this->get_items( 'shipping' ); + $total_shipping_tax_amount = 0; + foreach ( $line_items_shipping as $item_id => $shipping_item ) { + $tax_data = $shipping_item->get_taxes(); + if ( $tax_data ) { + foreach ( $order_taxes as $tax_item ) { + $tax_item_id = $tax_item->get_rate_id(); + $tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : ''; + $refunded = $this->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' ); + if ( $refunded ) { + $total_shipping_tax_amount += $tax_item_total - $refunded; + } else { + $total_shipping_tax_amount += $tax_item_total; + } + } + } + } + return $total_shipping_tax_amount / $order_items * $product_qty; + } + + /** + * Calculates coupon amount for specified line item/product. + * + * Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php. + * + * @param WC_Order_Item $item Line item from order. + * + * @return float + */ + public function get_item_coupon_amount( $item ) { + return floatval( $item->get_subtotal( 'edit' ) - $item->get_total( 'edit' ) ); + } +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-coupons-stats-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-coupons-stats-segmenting.php new file mode 100644 index 00000000000..dcb82d4040e --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-coupons-stats-segmenting.php @@ -0,0 +1,312 @@ + "SUM($products_table.coupon_amount) as amount", + ); + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Returns SELECT clause statements to be used for order-related product-level segmenting query (e.g. orders_count when segmented by category). + * + * @param string $coupons_lookup_table Name of SQL table containing the order-level segmenting info. + * + * @return string SELECT clause statements. + */ + protected function get_segment_selections_order_level( $coupons_lookup_table ) { + $columns_mapping = array( + 'coupons_count' => "COUNT(DISTINCT $coupons_lookup_table.coupon_id) as coupons_count", + 'orders_count' => "COUNT(DISTINCT $coupons_lookup_table.order_id) as orders_count", + ); + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Returns SELECT clause statements to be used for order-level segmenting query (e.g. discount amount when segmented by coupons). + * + * @param string $coupons_lookup_table Name of SQL table containing the order-level info. + * @param array $overrides Array of overrides for default column calculations. + * + * @return string + */ + protected function segment_selections_orders( $coupons_lookup_table, $overrides = array() ) { + $columns_mapping = array( + 'amount' => "SUM($coupons_lookup_table.discount_amount) as amount", + 'coupons_count' => "COUNT(DISTINCT $coupons_lookup_table.coupon_id) as coupons_count", + 'orders_count' => "COUNT(DISTINCT $coupons_lookup_table.order_id) as orders_count", + ); + + if ( $overrides ) { + $columns_mapping = array_merge( $columns_mapping, $overrides ); + } + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id). + * + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $totals_query Array of SQL clauses for totals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) { + global $wpdb; + + // Product-level numbers and order-level numbers can be fetched by the same query. + $segments_products = $wpdb->get_results( + "SELECT + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} + {$segmenting_selections['order_level']} + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $segmenting_groupby", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() ); + return $totals_segments; + } + + /** + * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id). + * + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $intervals_query Array of SQL clauses for intervals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) { + global $wpdb; + + // LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments. + $limit_parts = explode( ',', $intervals_query['limit'] ); + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + + // Product-level numbers and order-level numbers can be fetched by the same query. + $segments_products = $wpdb->get_results( + "SELECT + {$intervals_query['select_clause']} AS time_interval, + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} + {$segmenting_selections['order_level']} + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $segmenting_groupby + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() ); + return $intervals_segments; + } + + /** + * Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type). + * + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $totals_query Array of SQL clauses for intervals query. + * + * @return array + */ + protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) { + global $wpdb; + + $totals_segments = $wpdb->get_results( + "SELECT + $segmenting_groupby + $segmenting_select + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $segmenting_groupby", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Reformat result. + $totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby ); + return $totals_segments; + } + + /** + * Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type). + * + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $intervals_query Array of SQL clauses for intervals query. + * + * @return array + */ + protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) { + global $wpdb; + $limit_parts = explode( ',', $intervals_query['limit'] ); + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + + $intervals_segments = $wpdb->get_results( + "SELECT + MAX($table_name.date_created) AS datetime_anchor, + {$intervals_query['select_clause']} AS time_interval, + $segmenting_groupby + $segmenting_select + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $segmenting_groupby + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Reformat result. + $intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby ); + return $intervals_segments; + } + + /** + * Return array of segments formatted for REST response. + * + * @param string $type Type of segments to return--'totals' or 'intervals'. + * @param array $query_params SQL query parameter array. + * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). + * + * @return array + * @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified. + */ + protected function get_segments( $type, $query_params, $table_name ) { + global $wpdb; + if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) { + return array(); + } + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + $unique_orders_table = ''; + $segmenting_where = ''; + + // Product, variation, and category are bound to product, so here product segmenting table is required, + // while coupon and customer are bound to order, so we don't need the extra JOIN for those. + // This also means that segment selections need to be calculated differently. + if ( 'product' === $this->query_args['segmentby'] ) { + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $table_name ), + ); + $segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)"; + $segmenting_groupby = $product_segmenting_table . '.product_id'; + $segmenting_dimension_name = 'product_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'variation' === $this->query_args['segmentby'] ) { + if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) { + throw new WC_Admin_Reports_Parameter_Exception( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ) ); + } + + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $table_name ), + ); + $segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)"; + $segmenting_where = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}"; + $segmenting_groupby = $product_segmenting_table . '.variation_id'; + $segmenting_dimension_name = 'variation_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'category' === $this->query_args['segmentby'] ) { + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $table_name ), + ); + $segmenting_from = " + INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id) + LEFT JOIN {$wpdb->prefix}term_relationships ON {$product_segmenting_table}.product_id = {$wpdb->prefix}term_relationships.object_id + RIGHT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id + "; + $segmenting_where = " AND taxonomy = 'product_cat'"; + $segmenting_groupby = 'wp_term_taxonomy.term_taxonomy_id'; + $segmenting_dimension_name = 'category_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'coupon' === $this->query_args['segmentby'] ) { + $segmenting_selections = $this->segment_selections_orders( $table_name ); + $segmenting_from = ''; + $segmenting_groupby = "$table_name.coupon_id"; + + $segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ); + } + + return $segments; + } +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-stats-query.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-stats-query.php index c9c34b0ba84..2e097fe5bb3 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-stats-query.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-customers-stats-query.php @@ -34,7 +34,7 @@ class WC_Admin_Reports_Customers_Stats_Query extends WC_Admin_Reports_Query { 'page' => 1, 'order' => 'DESC', 'orderby' => 'date_registered', - 'fields' => '*', // TODO: needed? + 'fields' => '*', // @todo: needed? ); } diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php index 2bc1465c147..98bef2864d8 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -194,7 +194,7 @@ class WC_Admin_Reports_Interval { return (int) floor( ( (int) $diff_timestamp ) / DAY_IN_SECONDS ) + 1 + $addendum; case 'week': - // TODO: optimize? approximately day count / 7, but year end is tricky, a week can have fewer days. + // @todo: optimize? approximately day count / 7, but year end is tricky, a week can have fewer days. $week_count = 0; do { $start_datetime = self::next_week_start( $start_datetime ); diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-segmenting.php new file mode 100644 index 00000000000..c6757919802 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-segmenting.php @@ -0,0 +1,419 @@ + "SUM($products_table.product_qty) as num_items_sold", + 'gross_revenue' => "SUM($products_table.product_gross_revenue) AS gross_revenue", + 'coupons' => "SUM($products_table.coupon_amount) AS coupons", + 'refunds' => "SUM($products_table.refund_amount) AS refunds", + 'taxes' => "SUM($products_table.tax_amount) AS taxes", + 'shipping' => "SUM($products_table.shipping_amount) AS shipping", + // @todo: product_net_revenue should already have refunds subtracted, so it should not be here. Pls check. + 'net_revenue' => "SUM($products_table.product_net_revenue) AS net_revenue", + ); + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Returns SELECT clause statements to be used for order-related product-level segmenting query (e.g. avg items per order when segmented by category). + * + * @param string $unique_orders_table Name of SQL table containing the order-level segmenting info. + * + * @return string SELECT clause statements. + */ + protected function get_segment_selections_order_level( $unique_orders_table ) { + $columns_mapping = array( + 'orders_count' => "COUNT($unique_orders_table.order_id) AS orders_count", + 'avg_items_per_order' => "AVG($unique_orders_table.num_items_sold) AS avg_items_per_order", + 'avg_order_value' => "(SUM($unique_orders_table.net_total) - SUM($unique_orders_table.refund_total))/COUNT($unique_orders_table.order_id) AS avg_order_value", + 'num_returning_customers' => "SUM($unique_orders_table.returning_customer) AS num_returning_customers", + 'num_new_customers' => "COUNT($unique_orders_table.returning_customer) - SUM($unique_orders_table.returning_customer) AS num_new_customers", + ); + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Returns SELECT clause statements to be used for order-level segmenting query (e.g. avg items per order or net revenue when segmented by coupons). + * + * @param string $order_stats_table Name of SQL table containing the order-level info. + * @param array $overrides Array of overrides for default column calculations. + * + * @return string + */ + protected function segment_selections_orders( $order_stats_table, $overrides = array() ) { + $columns_mapping = array( + 'num_items_sold' => "SUM($order_stats_table.num_items_sold) as num_items_sold", + 'gross_revenue' => "SUM($order_stats_table.gross_total) AS gross_revenue", + 'coupons' => "SUM($order_stats_table.coupon_total) AS coupons", + 'refunds' => "SUM($order_stats_table.refund_total) AS refunds", + 'taxes' => "SUM($order_stats_table.tax_total) AS taxes", + 'shipping' => "SUM($order_stats_table.shipping_total) AS shipping", + 'net_revenue' => "SUM($order_stats_table.net_total) - SUM($order_stats_table.refund_total) AS net_revenue", + 'orders_count' => "COUNT($order_stats_table.order_id) AS orders_count", + 'avg_items_per_order' => "AVG($order_stats_table.num_items_sold) AS avg_items_per_order", + 'avg_order_value' => "(SUM($order_stats_table.net_total) - SUM($order_stats_table.refund_total))/COUNT($order_stats_table.order_id) AS avg_order_value", + 'num_returning_customers' => "SUM($order_stats_table.returning_customer) AS num_returning_customers", + 'num_new_customers' => "COUNT($order_stats_table.returning_customer) - SUM($order_stats_table.returning_customer) AS num_new_customers", + ); + + if ( $overrides ) { + $columns_mapping = array_merge( $columns_mapping, $overrides ); + } + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id). + * + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $totals_query Array of SQL clauses for totals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) { + global $wpdb; + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + + // Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued). + // Product-level numbers. + $segments_products = $wpdb->get_results( + "SELECT + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $segmenting_groupby", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Order level numbers. + // As there can be 2 same product ids (or variation ids) per one order, the orders first need to be uniqued before calculating averages, customer counts, etc. + $segments_orders = $wpdb->get_results( + "SELECT + $unique_orders_table.$segmenting_dimension_name AS $segmenting_dimension_name + {$segmenting_selections['order_level']} + FROM + ( + SELECT + $table_name.order_id, + $segmenting_groupby AS $segmenting_dimension_name, + MAX( num_items_sold ) AS num_items_sold, + MAX( net_total ) as net_total, + MAX( refund_total ) as refund_total, + MAX( returning_customer ) AS returning_customer + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $product_segmenting_table.order_id, $segmenting_groupby + ) AS $unique_orders_table + GROUP BY + $unique_orders_table.$segmenting_dimension_name", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, $segments_orders ); + return $totals_segments; + } + + /** + * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id). + * + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $intervals_query Array of SQL clauses for intervals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) { + global $wpdb; + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + + // LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments. + $limit_parts = explode( ',', $intervals_query['limit'] ); + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + + // Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued). + // Product-level numbers. + $segments_products = $wpdb->get_results( + "SELECT + {$intervals_query['select_clause']} AS time_interval, + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $segmenting_groupby + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Order level numbers. + // As there can be 2 same product ids (or variation ids) per one order, the orders first need to be uniqued before calculating averages, customer counts, etc. + $segments_orders = $wpdb->get_results( + "SELECT + $unique_orders_table.time_interval AS time_interval, + $unique_orders_table.$segmenting_dimension_name AS $segmenting_dimension_name + {$segmenting_selections['order_level']} + FROM + ( + SELECT + MAX( $table_name.date_created ) AS datetime_anchor, + {$intervals_query['select_clause']} AS time_interval, + $table_name.order_id, + $segmenting_groupby AS $segmenting_dimension_name, + MAX( num_items_sold ) AS num_items_sold, + MAX( net_total ) as net_total, + MAX( refund_total ) as refund_total, + MAX( returning_customer ) AS returning_customer + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $product_segmenting_table.order_id, $segmenting_groupby + ) AS $unique_orders_table + GROUP BY + time_interval, $unique_orders_table.$segmenting_dimension_name + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, $segments_orders ); + return $intervals_segments; + } + + /** + * Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type). + * + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $totals_query Array of SQL clauses for intervals query. + * + * @return array + */ + protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) { + global $wpdb; + + $totals_segments = $wpdb->get_results( + "SELECT + $segmenting_groupby + $segmenting_select + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $segmenting_groupby", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Reformat result. + $totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby ); + return $totals_segments; + } + + /** + * Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type). + * + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $intervals_query Array of SQL clauses for intervals query. + * + * @return array + */ + protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) { + global $wpdb; + $segmenting_limit = ''; + $limit_parts = explode( ',', $intervals_query['limit'] ); + if ( 2 === count( $limit_parts ) ) { + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + } + + $intervals_segments = $wpdb->get_results( + "SELECT + MAX($table_name.date_created) AS datetime_anchor, + {$intervals_query['select_clause']} AS time_interval, + $segmenting_groupby + $segmenting_select + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $segmenting_groupby + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Reformat result. + $intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby ); + return $intervals_segments; + } + + /** + * Return array of segments formatted for REST response. + * + * @param string $type Type of segments to return--'totals' or 'intervals'. + * @param array $query_params SQL query parameter array. + * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). + * + * @return array + * @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified. + */ + protected function get_segments( $type, $query_params, $table_name ) { + global $wpdb; + if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) { + return array(); + } + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + $unique_orders_table = 'uniq_orders'; + $segmenting_where = ''; + + // Product, variation, and category are bound to product, so here product segmenting table is required, + // while coupon and customer are bound to order, so we don't need the extra JOIN for those. + // This also means that segment selections need to be calculated differently. + if ( 'product' === $this->query_args['segmentby'] ) { + // @todo: how to handle shipping taxes when grouped by product? + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $unique_orders_table ), + ); + $segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)"; + $segmenting_groupby = $product_segmenting_table . '.product_id'; + $segmenting_dimension_name = 'product_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'variation' === $this->query_args['segmentby'] ) { + if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) { + throw new WC_Admin_Reports_Parameter_Exception( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ) ); + } + + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $unique_orders_table ), + ); + $segmenting_from = "INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id)"; + $segmenting_where = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}"; + $segmenting_groupby = $product_segmenting_table . '.variation_id'; + $segmenting_dimension_name = 'variation_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'category' === $this->query_args['segmentby'] ) { + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $unique_orders_table ), + ); + $segmenting_from = " + INNER JOIN $product_segmenting_table ON ($table_name.order_id = $product_segmenting_table.order_id) + LEFT JOIN {$wpdb->prefix}term_relationships ON {$product_segmenting_table}.product_id = {$wpdb->prefix}term_relationships.object_id + RIGHT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id + "; + $segmenting_where = " AND taxonomy = 'product_cat'"; + $segmenting_groupby = 'wp_term_taxonomy.term_taxonomy_id'; + $segmenting_dimension_name = 'category_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'coupon' === $this->query_args['segmentby'] ) { + // As there can be 2 or more coupons applied per one order, coupon amount needs to be split. + $coupon_override = array( + 'coupons' => 'SUM(coupon_lookup.discount_amount) AS coupons', + ); + $segmenting_selections = $this->segment_selections_orders( $table_name, $coupon_override ); + $segmenting_from = " + INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($table_name.order_id = coupon_lookup.order_id) + "; + $segmenting_groupby = 'coupon_lookup.coupon_id'; + + $segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ); + } elseif ( 'customer_type' === $this->query_args['segmentby'] ) { + $segmenting_selections = $this->segment_selections_orders( $table_name ); + $segmenting_from = ''; + $segmenting_groupby = "$table_name.returning_customer"; + + $segments = $this->get_order_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ); + } + + return $segments; + } +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-parameter-exception.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-parameter-exception.php new file mode 100644 index 00000000000..75231f7d595 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-parameter-exception.php @@ -0,0 +1,15 @@ + "SUM($products_table.product_qty) as items_sold", + 'net_revenue' => "SUM($products_table.product_net_revenue ) AS net_revenue", + 'orders_count' => "COUNT( DISTINCT $products_table.order_id ) AS orders_count", + 'products_count' => "COUNT( DISTINCT $products_table.product_id ) AS products_count", + ); + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id). + * + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $totals_query Array of SQL clauses for totals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) { + global $wpdb; + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + + // Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued). + // Product-level numbers. + $segments_products = $wpdb->get_results( + "SELECT + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $segmenting_groupby", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $totals_segments = $this->merge_segment_totals_results( $segmenting_dimension_name, $segments_products, array() ); + return $totals_segments; + } + + /** + * Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id). + * + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $intervals_query Array of SQL clauses for intervals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) { + global $wpdb; + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + + // LIMIT offset, rowcount needs to be updated to LIMIT offset, rowcount * max number of segments. + $limit_parts = explode( ',', $intervals_query['limit'] ); + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + + // Can't get all the numbers from one query, so split it into one query for product-level numbers and one for order-level numbers (which first need to have orders uniqued). + // Product-level numbers. + $segments_products = $wpdb->get_results( + "SELECT + {$intervals_query['select_clause']} AS time_interval, + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $segmenting_groupby + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + $intervals_segments = $this->merge_segment_intervals_results( $segmenting_dimension_name, $segments_products, array() ); + return $intervals_segments; + } + + /** + * Return array of segments formatted for REST response. + * + * @param string $type Type of segments to return--'totals' or 'intervals'. + * @param array $query_params SQL query parameter array. + * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). + * + * @return array + * @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified. + */ + protected function get_segments( $type, $query_params, $table_name ) { + global $wpdb; + if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) { + return array(); + } + + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + $unique_orders_table = 'uniq_orders'; + $segmenting_where = ''; + + // Product, variation, and category are bound to product, so here product segmenting table is required, + // while coupon and customer are bound to order, so we don't need the extra JOIN for those. + // This also means that segment selections need to be calculated differently. + if ( 'product' === $this->query_args['segmentby'] ) { + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + ); + $segmenting_from = ''; + $segmenting_groupby = $product_segmenting_table . '.product_id'; + $segmenting_dimension_name = 'product_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'variation' === $this->query_args['segmentby'] ) { + if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) { + throw new WC_Admin_Reports_Parameter_Exception( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ) ); + } + + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + ); + $segmenting_from = ''; + $segmenting_where = "AND $product_segmenting_table.product_id = {$this->query_args['product_includes'][0]}"; + $segmenting_groupby = $product_segmenting_table . '.variation_id'; + $segmenting_dimension_name = 'variation_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'category' === $this->query_args['segmentby'] ) { + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $product_segmenting_table ), + ); + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}term_relationships ON {$product_segmenting_table}.product_id = {$wpdb->prefix}term_relationships.object_id + RIGHT JOIN {$wpdb->prefix}term_taxonomy ON {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_taxonomy_id + "; + $segmenting_where = " AND taxonomy = 'product_cat'"; + $segmenting_groupby = 'wp_term_taxonomy.term_taxonomy_id'; + $segmenting_dimension_name = 'category_id'; + + $segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } + + return $segments; + } +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php new file mode 100644 index 00000000000..d111d8044a3 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php @@ -0,0 +1,520 @@ +query_args = $query_args; + $this->report_columns = $report_columns; + } + + /** + * Filters definitions for SELECT clauses based on query_args and joins them into one string usable in SELECT clause. + * + * @param array $columns_mapping Column name -> SQL statememt mapping. + * + * @return string to be used in SELECT clause statements. + */ + protected function prepare_selections( $columns_mapping ) { + if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) { + $keep = array(); + foreach ( $this->query_args['fields'] as $field ) { + if ( isset( $columns_mapping[ $field ] ) ) { + $keep[ $field ] = $columns_mapping[ $field ]; + } + } + $selections = implode( ', ', $keep ); + } else { + $selections = implode( ', ', $columns_mapping ); + } + + if ( $selections ) { + $selections = ',' . $selections; + } + + return $selections; + } + + /** + * Update row-level db result for segments in 'totals' section to the format used for output. + * + * @param array $segments_db_result Results from the SQL db query for segmenting. + * @param string $segment_dimension Name of column used for grouping the result. + * + * @return array Reformatted array. + */ + protected function reformat_totals_segments( $segments_db_result, $segment_dimension ) { + $segment_result = array(); + + if ( strpos( $segment_dimension, '.' ) ) { + $segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 ); + } + + foreach ( $segments_db_result as $segment_data ) { + $segment_id = $segment_data[ $segment_dimension ]; + unset( $segment_data[ $segment_dimension ] ); + $segment_datum = array( + 'segment_id' => $segment_id, + 'subtotals' => $segment_data, + ); + $segment_result[ $segment_id ] = $segment_datum; + } + + return $segment_result; + } + + /** + * Merges segmented results for totals response part. + * + * E.g. $r1 = array( + * 0 => array( + * 'product_id' => 3, + * 'net_amount' => 15, + * ), + * ); + * $r2 = array( + * 0 => array( + * 'product_id' => 3, + * 'avg_order_value' => 25, + * ), + * ); + * + * $merged = array( + * 3 => array( + * 'segment_id' => 3, + * 'subtotals' => array( + * 'net_amount' => 15, + * 'avg_order_value' => 25, + * ) + * ), + * ); + * + * @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets. + * @param array $result1 Array 1 of segmented figures. + * @param array $result2 Array 2 of segmented figures. + * + * @return array + */ + protected function merge_segment_totals_results( $segment_dimension, $result1, $result2 ) { + $result_segments = array(); + + foreach ( $result1 as $segment_data ) { + $segment_id = $segment_data[ $segment_dimension ]; + unset( $segment_data[ $segment_dimension ] ); + $result_segments[ $segment_id ] = array( + 'segment_id' => $segment_id, + 'subtotals' => $segment_data, + ); + } + + foreach ( $result2 as $segment_data ) { + $segment_id = $segment_data[ $segment_dimension ]; + unset( $segment_data[ $segment_dimension ] ); + if ( ! isset( $result_segments[ $segment_id ] ) ) { + $result_segments[ $segment_id ] = array( + 'segment_id' => $segment_id, + 'subtotals' => array(), + ); + } + $result_segments[ $segment_id ]['subtotals'] = array_merge( $result_segments[ $segment_id ]['subtotals'], $segment_data ); + } + return $result_segments; + } + /** + * Merges segmented results for intervals response part. + * + * E.g. $r1 = array( + * 0 => array( + * 'product_id' => 3, + * 'time_interval' => '2018-12' + * 'net_amount' => 15, + * ), + * ); + * $r2 = array( + * 0 => array( + * 'product_id' => 3, + * 'time_interval' => '2018-12' + * 'avg_order_value' => 25, + * ), + * ); + * + * $merged = array( + * '2018-12' => array( + * 'segments' => array( + * 3 => array( + * 'segment_id' => 3, + * 'subtotals' => array( + * 'net_amount' => 15, + * 'avg_order_value' => 25, + * ), + * ), + * ), + * ), + * ); + * + * @param string $segment_dimension Name of the segment dimension=key in the result arrays used to match records from result sets. + * @param array $result1 Array 1 of segmented figures. + * @param array $result2 Array 2 of segmented figures. + * + * @return array + */ + protected function merge_segment_intervals_results( $segment_dimension, $result1, $result2 ) { + $result_segments = array(); + + foreach ( $result1 as $segment_data ) { + $time_interval = $segment_data['time_interval']; + if ( ! isset( $result_segments[ $time_interval ] ) ) { + $result_segments[ $time_interval ] = array(); + $result_segments[ $time_interval ]['segments'] = array(); + } + unset( $segment_data['time_interval'] ); + unset( $segment_data['datetime_anchor'] ); + $segment_id = $segment_data[ $segment_dimension ]; + unset( $segment_data[ $segment_dimension ] ); + $segment_datum = array( + 'segment_id' => $segment_id, + 'subtotals' => $segment_data, + ); + $result_segments[ $time_interval ]['segments'][ $segment_id ] = $segment_datum; + } + + foreach ( $result2 as $segment_data ) { + $time_interval = $segment_data['time_interval']; + if ( ! isset( $result_segments[ $time_interval ] ) ) { + $result_segments[ $time_interval ] = array(); + $result_segments[ $time_interval ]['segments'] = array(); + } + unset( $segment_data['time_interval'] ); + unset( $segment_data['datetime_anchor'] ); + $segment_id = $segment_data[ $segment_dimension ]; + unset( $segment_data[ $segment_dimension ] ); + + if ( ! isset( $result_segments[ $time_interval ]['segments'][ $segment_id ] ) ) { + $result_segments[ $time_interval ]['segments'][ $segment_id ] = array( + 'segment_id' => $segment_id, + 'subtotals' => array(), + ); + } + $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'] = array_merge( $result_segments[ $time_interval ]['segments'][ $segment_id ]['subtotals'], $segment_data ); + } + return $result_segments; + } + + /** + * Update row-level db result for segments in 'intervals' section to the format used for output. + * + * @param array $segments_db_result Results from the SQL db query for segmenting. + * @param string $segment_dimension Name of column used for grouping the result. + * + * @return array Reformatted array. + */ + protected function reformat_intervals_segments( $segments_db_result, $segment_dimension ) { + $aggregated_segment_result = array(); + + if ( strpos( $segment_dimension, '.' ) ) { + $segment_dimension = substr( strstr( $segment_dimension, '.' ), 1 ); + } + + foreach ( $segments_db_result as $segment_data ) { + $time_interval = $segment_data['time_interval']; + if ( ! isset( $aggregated_segment_result[ $time_interval ] ) ) { + $aggregated_segment_result[ $time_interval ] = array(); + $aggregated_segment_result[ $time_interval ]['segments'] = array(); + } + unset( $segment_data['time_interval'] ); + unset( $segment_data['datetime_anchor'] ); + $segment_id = $segment_data[ $segment_dimension ]; + unset( $segment_data[ $segment_dimension ] ); + $segment_datum = array( + 'segment_id' => $segment_id, + 'subtotals' => $segment_data, + ); + $aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum; + } + + return $aggregated_segment_result; + } + + /** + * Fetches all segment ids from db and stores it for later use. + * + * @return void + */ + protected function set_all_segments() { + global $wpdb; + + if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) { + $this->all_segment_ids = array(); + return; + } + + if ( 'product' === $this->query_args['segmentby'] ) { + $segments = wc_get_products( + array( + 'return' => 'ids', + 'limit' => -1, + ) + ); + } elseif ( 'variation' === $this->query_args['segmentby'] ) { + // @todo: assuming that this will only be used for one product, check assumption. + if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) { + $this->all_segment_ids = array(); + return; + } + + $segments = wc_get_products( + array( + 'return' => 'ids', + 'limit' => - 1, + 'type' => 'variation', + 'parent' => $this->query_args['product_includes'][0], + ) + ); + } elseif ( 'category' === $this->query_args['segmentby'] ) { + $categories = get_categories( + array( + 'taxonomy' => 'product_cat', + ) + ); + $segments = wp_list_pluck( $categories, 'cat_ID' ); + } elseif ( 'coupon' === $this->query_args['segmentby'] ) { + // @todo: switch to a non-direct-SQL way to get all coupons? + // @todo: These are only currently existing coupons, but we should add also deleted ones, if they have been used at least once. + $coupon_ids = $wpdb->get_results( "SELECT ID FROM {$wpdb->prefix}posts WHERE post_type='shop_coupon' AND post_status='publish'", ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + $segments = wp_list_pluck( $coupon_ids, 'ID' ); + } elseif ( 'customer_type' === $this->query_args['segmentby'] ) { + // 0 -- new customer + // 1 -- returning customer + $segments = array( 0, 1 ); + } elseif ( 'tax_rate_id' === $this->query_args['segmentby'] ) { + // @todo: do we need to include tax rates that existed in the past, but have never been used? I guess there are other, more pressing problems... + // Current tax rates UNION previously used tax rates. + $tax_rate_ids = $wpdb->get_results( + "SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates + UNION + SELECT DISTINCT meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta where meta_key='rate_id'", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + $segments = wp_list_pluck( $tax_rate_ids, 'tax_rate_id' ); + } else { + // Catch all default. + $segments = array(); + } + + $this->all_segment_ids = $segments; + } + + /** + * Return all segment ids for given segmentby query parameter. + * + * @return array + */ + protected function get_all_segments() { + if ( ! is_array( $this->all_segment_ids ) ) { + $this->set_all_segments(); + } + + return $this->all_segment_ids; + } + + /** + * Compares two report data objects by pre-defined object property and ASC/DESC ordering. + * + * @param stdClass $a Object a. + * @param stdClass $b Object b. + * @return string + */ + private function segment_cmp( $a, $b ) { + if ( $a['segment_id'] === $b['segment_id'] ) { + return 0; + } elseif ( $a['segment_id'] > $b['segment_id'] ) { + return 1; + } elseif ( $a['segment_id'] < $b['segment_id'] ) { + return - 1; + } + } + + /** + * Adds zeroes for segments not present in the data selection. + * + * @param array $segments Array of segments from the database for given data points. + * + * @return array + */ + protected function fill_in_missing_segments( $segments ) { + + $segment_subtotals = array(); + if ( isset( $this->query_args['fields'] ) && is_array( $this->query_args['fields'] ) ) { + foreach ( $this->query_args['fields'] as $field ) { + if ( isset( $this->report_columns[ $field ] ) ) { + $segment_subtotals[ $field ] = 0; + } + } + } else { + foreach ( $this->report_columns as $field => $sql_clause ) { + $segment_subtotals[ $field ] = 0; + } + } + if ( ! is_array( $segments ) ) { + $segments = array(); + } + $all_segment_ids = $this->get_all_segments(); + foreach ( $all_segment_ids as $segment_id ) { + if ( ! isset( $segments[ $segment_id ] ) ) { + $segments[ $segment_id ] = array( + 'segment_id' => $segment_id, + 'subtotals' => $segment_subtotals, + ); + } + } + + // Using array_values to remove custom keys, so that it gets later converted to JSON as an array. + $segments_no_keys = array_values( $segments ); + usort( $segments_no_keys, array( $this, 'segment_cmp' ) ); + return $segments_no_keys; + } + + /** + * Adds missing segments to intervals, modifies $data. + * + * @param stdClass $data Response data. + */ + protected function fill_in_missing_interval_segments( &$data ) { + foreach ( $data->intervals as $order_id => $interval_data ) { + $data->intervals[ $order_id ]['segments'] = $this->fill_in_missing_segments( $data->intervals[ $order_id ]['segments'] ); + } + } + + /** + * Calculate segments for segmenting property bound to product (e.g. category, product_id, variation_id). + * + * @param string $type Type of segments to return--'totals' or 'intervals'. + * @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $segmenting_dimension_name Name of the segmenting dimension. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $query_params Array of SQL clauses for intervals/totals query. + * @param string $unique_orders_table Name of temporary SQL table that holds unique orders. + * + * @return array + */ + protected function get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ) { + if ( 'totals' === $type ) { + return $this->get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } elseif ( 'intervals' === $type ) { + return $this->get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table ); + } + } + + /** + * Calculate segments for segmenting property bound to order (e.g. coupon or customer type). + * + * @param string $type Type of segments to return--'totals' or 'intervals'. + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $query_params Array of SQL clauses for intervals/totals query. + * + * @return array + */ + protected function get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ) { + if ( 'totals' === $type ) { + return $this->get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ); + } elseif ( 'intervals' === $type ) { + return $this->get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ); + } + } + + /** + * Assign segments to time intervals by updating original $intervals array. + * + * @param array $intervals Result array from intervals SQL query. + * @param array $intervals_segments Result array from interval segments SQL query. + */ + protected function assign_segments_to_intervals( &$intervals, $intervals_segments ) { + $old_keys = array_keys( $intervals ); + foreach ( $intervals as $interval ) { + $intervals[ $interval['time_interval'] ] = $interval; + $intervals[ $interval['time_interval'] ]['segments'] = array(); + } + foreach ( $old_keys as $key ) { + unset( $intervals[ $key ] ); + } + + foreach ( $intervals_segments as $time_interval => $segment ) { + if ( ! isset( $intervals[ $time_interval ] ) ) { + $intervals[ $time_interval ]['segments'] = array(); + } + $intervals[ $time_interval ]['segments'] = $segment['segments']; + } + + // To remove time interval keys (so that REST response is formatted correctly). + $intervals = array_values( $intervals ); + } + + /** + * Returns an array of segments for totals part of REST response. + * + * @param array $query_params Totals SQL query parameters. + * @param string $table_name Name of the SQL table that is the main order stats table. + * + * @return array + */ + public function get_totals_segments( $query_params, $table_name ) { + $segments = $this->get_segments( 'totals', $query_params, $table_name ); + return $this->fill_in_missing_segments( $segments ); + } + + /** + * Adds an array of segments to data->intervals object. + * + * @param stdClass $data Data object representing the REST response. + * @param array $intervals_query Intervals SQL query parameters. + * @param string $table_name Name of the SQL table that is the main order stats table. + */ + public function add_intervals_segments( &$data, $intervals_query, $table_name ) { + $intervals_segments = $this->get_segments( 'intervals', $intervals_query, $table_name ); + $this->assign_segments_to_intervals( $data->intervals, $intervals_segments ); + $this->fill_in_missing_interval_segments( $data ); + } +} diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-taxes-stats-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-taxes-stats-segmenting.php new file mode 100644 index 00000000000..6f3bf61bada --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-taxes-stats-segmenting.php @@ -0,0 +1,147 @@ + "COUNT(DISTINCT $lookup_table.tax_rate_id) as tax_codes", + 'total_tax' => "SUM($lookup_table.total_tax) AS total_tax", + 'order_tax' => "SUM($lookup_table.order_tax) as order_tax", + 'shipping_tax' => "SUM($lookup_table.shipping_tax) as shipping_tax", + 'orders_count' => "COUNT(DISTINCT $lookup_table.order_id) as orders_count", + ); + + return $this->prepare_selections( $columns_mapping ); + } + + /** + * Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type). + * + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $totals_query Array of SQL clauses for intervals query. + * + * @return array + */ + protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) { + global $wpdb; + + $totals_segments = $wpdb->get_results( + "SELECT + $segmenting_groupby + $segmenting_select + FROM + $table_name + $segmenting_from + {$totals_query['from_clause']} + WHERE + 1=1 + {$totals_query['where_time_clause']} + {$totals_query['where_clause']} + $segmenting_where + GROUP BY + $segmenting_groupby", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Reformat result. + $totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby ); + return $totals_segments; + } + + /** + * Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type). + * + * @param string $segmenting_select SELECT part of segmenting SQL query. + * @param string $segmenting_from FROM part of segmenting SQL query. + * @param string $segmenting_where WHERE part of segmenting SQL query. + * @param string $segmenting_groupby GROUP BY part of segmenting SQL query. + * @param string $table_name Name of SQL table which is the stats table for orders. + * @param array $intervals_query Array of SQL clauses for intervals query. + * + * @return array + */ + protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) { + global $wpdb; + $segmenting_limit = ''; + $limit_parts = explode( ',', $intervals_query['limit'] ); + if ( 2 === count( $limit_parts ) ) { + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + } + + $intervals_segments = $wpdb->get_results( + "SELECT + MAX($table_name.date_created) AS datetime_anchor, + {$intervals_query['select_clause']} AS time_interval, + $segmenting_groupby + $segmenting_select + FROM + $table_name + $segmenting_from + {$intervals_query['from_clause']} + WHERE + 1=1 + {$intervals_query['where_time_clause']} + {$intervals_query['where_clause']} + $segmenting_where + GROUP BY + time_interval, $segmenting_groupby + $segmenting_limit", + ARRAY_A + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + // Reformat result. + $intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby ); + return $intervals_segments; + } + + /** + * Return array of segments formatted for REST response. + * + * @param string $type Type of segments to return--'totals' or 'intervals'. + * @param array $query_params SQL query parameter array. + * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). + * + * @return array + * @throws WC_Admin_Reports_Parameter_Exception In case of segmenting by variations, when no parent product is specified. + */ + protected function get_segments( $type, $query_params, $table_name ) { + if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) { + return array(); + } + + $segmenting_where = ''; + $segmenting_from = ''; + $segments = array(); + + if ( 'tax_rate_id' === $this->query_args['segmentby'] ) { + $segmenting_select = $this->get_segment_selections_order_level( $table_name ); + $segmenting_groupby = $table_name . '.tax_rate_id'; + + $segments = $this->get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params ); + } + + return $segments; + } +} diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php index 2d81340813f..9b12ffc66bb 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-categories-data-store.php @@ -95,7 +95,7 @@ class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store $sql_query_params['where_clause'] .= " AND {$wpdb->prefix}term_taxonomy.term_id IN ({$included_categories})"; } - // TODO: only products in the category C or orders with products from category C (and, possibly others?). + // @todo: only products in the category C or orders with products from category C (and, possibly others?). $included_products = $this->get_included_products( $query_args ); if ( $included_products ) { $sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$included_products})"; diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-coupons-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-coupons-stats-data-store.php index 395eca39504..ea591f68390 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-coupons-stats-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-coupons-stats-data-store.php @@ -165,7 +165,9 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons if ( null === $totals ) { return $data; } - $totals = (object) $this->cast_numbers( $totals[0] ); + $segmenter = new WC_Admin_Reports_Coupons_Stats_Segmenting( $query_args, $this->report_columns ); + $totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name ); + $totals = (object) $this->cast_numbers( $totals[0] ); // Intervals. $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name ); @@ -213,6 +215,7 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons } else { $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); } + $segmenter->add_intervals_segments( $data, $intervals_query, $table_name ); $this->create_interval_subtotals( $data->intervals ); wp_cache_set( $cache_key, $data, $this->cache_group ); 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 1c0d3cba50f..d6ec7196bc2 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 @@ -41,7 +41,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store '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? + 'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo: what does this mean for RTL? 'email' => 'email', 'country' => 'country', 'city' => 'city', diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php index a8b5a5d4b79..56e729736ca 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-data-store.php @@ -49,7 +49,7 @@ class WC_Admin_Reports_Data_Store { */ protected $report_columns = array(); - // TODO: this does not really belong here, maybe factor out the comparison as separate class? + // @todo: this does not really belong here, maybe factor out the comparison as separate class? /** * Order by property, used in the cmp function. * @@ -73,7 +73,7 @@ class WC_Admin_Reports_Data_Store { private function interval_cmp( $a, $b ) { if ( '' === $this->order_by || '' === $this->order ) { return 0; - // TODO: should return WP_Error here perhaps? + // @todo: should return WP_Error here perhaps? } if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) { return 0; @@ -94,9 +94,20 @@ class WC_Admin_Reports_Data_Store { * @param string $direction DESC/ASC. */ protected function sort_intervals( &$data, $sort_by, $direction ) { + $this->sort_array( $data->intervals, $sort_by, $direction ); + } + + /** + * Sorts array of arrays based on subarray key $sort_by. + * + * @param array $arr Array to sort. + * @param string $sort_by Ordering property. + * @param string $direction DESC/ASC. + */ + protected function sort_array( &$arr, $sort_by, $direction ) { $this->order_by = $this->normalize_order_by( $sort_by ); $this->order = $direction; - usort( $data->intervals, array( $this, 'interval_cmp' ) ); + usort( $arr, array( $this, 'interval_cmp' ) ); } /** @@ -110,7 +121,7 @@ class WC_Admin_Reports_Data_Store { * @return stdClass */ protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) { - // TODO: this is ugly and messy. + // @todo: this is ugly and messy. // At this point, we don't know when we can stop iterating, as the ordering can be based on any value. $end_datetime = new DateTime( $datetime_end ); $time_ids = array_flip( wp_list_pluck( $data->intervals, 'time_interval' ) ); @@ -121,7 +132,7 @@ class WC_Admin_Reports_Data_Store { foreach ( $totals_arr as $key => $val ) { $totals_arr[ $key ] = 0; } - // TODO: should 'products' be in intervals? + // @todo: should 'products' be in intervals? unset( $totals_arr['products'] ); while ( $datetime <= $end_datetime ) { $next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval ); @@ -325,7 +336,7 @@ class WC_Admin_Reports_Data_Store { $start_iteration = 0; } if ( $start_iteration ) { - // TODO: is this correct? should it only be added if iterate runs? other two iterate instances, too? + // @todo: is this correct? should it only be added if iterate runs? other two iterate instances, too? $new_start_date_timestamp = (int) $new_start_date->format( 'U' ) + 1; $new_start_date->setTimestamp( $new_start_date_timestamp ); } @@ -451,7 +462,7 @@ class WC_Admin_Reports_Data_Store { $datetime = new DateTime( $interval['datetime_anchor'] ); $prev_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval, true ); - // TODO: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below. + // @todo: not sure if the +1/-1 here are correct, especially as they are applied before the ?: below. $prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1; $prev_start->setTimestamp( $prev_start_timestamp ); if ( $datetime_start ) { 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 index ad9c425f197..5ea9cd13fe8 100644 --- 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 @@ -45,6 +45,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto 'num_returning_customers' => 'intval', 'num_new_customers' => 'intval', 'products' => 'intval', + 'segment_id' => 'intval', ); /** @@ -73,7 +74,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto */ public static function init() { add_action( 'save_post', array( __CLASS__, 'sync_order' ) ); - // TODO: this is required as order update skips save_post. + // @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 ); @@ -88,7 +89,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto * @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? + // @todo: performance of all of this? global $wpdb; $from_clause = ''; @@ -97,7 +98,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto $where_filters = array(); - // TODO: maybe move the sql inside the get_included/excluded functions? + // @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 ); @@ -176,7 +177,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto /** * 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. */ @@ -197,6 +197,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), 'interval' => 'week', 'fields' => '*', + 'segmentby' => '', 'match' => 'all', 'status_is' => array(), @@ -248,7 +249,10 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto $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] ); + $segmenting = new WC_Admin_Reports_Orders_Stats_Segmenting( $query_args, $this->report_columns ); + $totals[0]['segments'] = $segmenting->get_totals_segments( $totals_query, $table_name ); + + $totals = (object) $this->cast_numbers( $totals[0] ); $db_intervals = $wpdb->get_col( "SELECT @@ -317,6 +321,7 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto } else { $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); } + $segmenting->add_intervals_segments( $data, $intervals_query, $table_name ); $this->create_interval_subtotals( $data->intervals ); wp_cache_set( $cache_key, $data, $this->cache_group ); diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php index f663d8c366c..e1c0ce9d89e 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-data-store.php @@ -331,67 +331,74 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i return; } - $refunds = self::get_order_refund_items( $order ); - foreach ( $order->get_items() as $order_item ) { - $order_item_id = $order_item->get_id(); - $quantity_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['quantity'] : 0; - $amount_refunded = isset( $refunds[ $order_item_id ] ) ? $refunds[ $order_item_id ]['subtotal'] : 0; + $order_item_id = $order_item->get_id(); + $quantity_refunded = $order->get_item_quantity_refunded( $order_item ); + $amount_refunded = $order->get_item_amount_refunded( $order_item ); + $product_qty = $order->get_item_quantity_minus_refunded( $order_item ); + $shipping_amount = $order->get_item_shipping_amount( $order_item ); + $shipping_tax_amount = $order->get_item_shipping_tax_amount( $order_item ); + $coupon_amount = $order->get_item_coupon_amount( $order_item ); + + // Tax amount. + // @todo: check if this calculates tax correctly with refunds. + $tax_amount = 0; + + $order_taxes = $order->get_taxes(); + $tax_data = $order_item->get_taxes(); + foreach ( $order_taxes as $tax_item ) { + $tax_item_id = $tax_item->get_rate_id(); + $tax_amount += isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : 0; + } + + // @todo: should net revenue be affected by refunds, as refunds are tracked separately? + $net_revenue = $order_item->get_subtotal( 'edit' ) - $amount_refunded; + if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) { $wpdb->delete( $wpdb->prefix . self::TABLE_NAME, array( 'order_item_id' => $order_item_id ), array( '%d' ) - ); + ); // WPCS: cache ok, DB call ok. } else { $wpdb->replace( $wpdb->prefix . self::TABLE_NAME, array( - 'order_item_id' => $order_item_id, - 'order_id' => $order->get_id(), - 'product_id' => $order_item->get_product_id( 'edit' ), - 'variation_id' => $order_item->get_variation_id( 'edit' ), - 'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null, - 'product_qty' => $order_item->get_quantity( 'edit' ) - $quantity_refunded, - 'product_net_revenue' => $order_item->get_subtotal( 'edit' ) - $amount_refunded, - 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'order_item_id' => $order_item_id, + 'order_id' => $order->get_id(), + 'product_id' => $order_item->get_product_id( 'edit' ), + 'variation_id' => $order_item->get_variation_id( 'edit' ), + 'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null, + 'product_qty' => $product_qty, + 'product_net_revenue' => $net_revenue, + 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'coupon_amount' => $coupon_amount, + 'tax_amount' => $tax_amount, + 'shipping_amount' => $shipping_amount, + 'shipping_tax_amount' => $shipping_tax_amount, + // @todo: can this be incorrect if modified by filters? + 'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount, + 'refund_amount' => $amount_refunded, ), array( - '%d', - '%d', - '%d', - '%d', - '%d', - '%d', - '%f', - '%s', + '%d', // order_item_id. + '%d', // order_id. + '%d', // product_id. + '%d', // variation_id. + '%d', // customer_id. + '%d', // product_qty. + '%f', // product_net_revenue. + '%s', // date_created. + '%f', // coupon_amount. + '%f', // tax_amount. + '%f', // shipping_amount. + '%f', // shipping_tax_amount. + '%f', // product_gross_revenue. + '%f', // refund_amount. ) - ); + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. } } } - /** - * Get order refund items quantity and subtotal - * - * @param object $order WC Order object. - * @return array - */ - public static function get_order_refund_items( $order ) { - $refunds = $order->get_refunds(); - $refunded_line_items = array(); - foreach ( $refunds as $refund ) { - foreach ( $refund->get_items() as $refunded_item ) { - $line_item_id = wc_get_order_item_meta( $refunded_item->get_id(), '_refunded_item_id', true ); - if ( ! isset( $refunded_line_items[ $line_item_id ] ) ) { - $refunded_line_items[ $line_item_id ]['quantity'] = 0; - $refunded_line_items[ $line_item_id ]['subtotal'] = 0; - } - $refunded_line_items[ $line_item_id ]['quantity'] += absint( $refunded_item['quantity'] ); - $refunded_line_items[ $line_item_id ]['subtotal'] += abs( $refunded_item['subtotal'] ); - } - } - return $refunded_line_items; - } - } diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-stats-data-store.php index f310418ec61..1e907a2b98f 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-stats-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-products-stats-data-store.php @@ -159,6 +159,9 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + $segmenter = new WC_Admin_Reports_Products_Stats_Segmenting( $query_args, $this->report_columns ); + $totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name ); + if ( null === $totals ) { return new WP_Error( 'woocommerce_reports_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); } @@ -208,6 +211,7 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc } else { $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); } + $segmenter->add_intervals_segments( $data, $intervals_query, $table_name ); $this->create_interval_subtotals( $data->intervals ); wp_cache_set( $cache_key, $data, $this->cache_group ); diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-taxes-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-taxes-stats-data-store.php index 365febbfba0..513228d1343 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-taxes-stats-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-taxes-stats-data-store.php @@ -183,6 +183,8 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor if ( null === $totals ) { return new WP_Error( 'woocommerce_reports_taxes_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); } + $segmenter = new WC_Admin_Reports_Taxes_Stats_Segmenting( $query_args, $this->report_columns ); + $totals[0]['segments'] = $segmenter->get_totals_segments( $totals_query, $table_name ); $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name ); @@ -231,6 +233,7 @@ class WC_Admin_Reports_Taxes_Stats_Data_Store extends WC_Admin_Reports_Data_Stor } else { $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); } + $segmenter->add_intervals_segments( $data, $intervals_query, $table_name ); $this->create_interval_subtotals( $data->intervals ); wp_cache_set( $cache_key, $data, $this->cache_group ); diff --git a/plugins/woocommerce-admin/includes/wc-admin-order-functions.php b/plugins/woocommerce-admin/includes/wc-admin-order-functions.php new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/woocommerce-admin/tests/api/reports-coupons-stats.php b/plugins/woocommerce-admin/tests/api/reports-coupons-stats.php index 0a4abfc9f08..eb09fa697f6 100644 --- a/plugins/woocommerce-admin/tests/api/reports-coupons-stats.php +++ b/plugins/woocommerce-admin/tests/api/reports-coupons-stats.php @@ -101,9 +101,10 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case { $expected_reports = array( 'totals' => array( - 'amount' => 4, + 'amount' => 4.0, 'coupons_count' => 2, 'orders_count' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -113,9 +114,10 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case { 'date_end' => date( 'Y-m-d 23:59:59', $time ), 'date_end_gmt' => date( 'Y-m-d 23:59:59', $time ), 'subtotals' => (object) array( - 'amount' => 4, + 'amount' => 4.0, 'coupons_count' => 2, 'orders_count' => 2, + 'segments' => array(), ), ), ), @@ -150,10 +152,11 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'intervals', $properties ); $totals = $properties['totals']['properties']; - $this->assertEquals( 3, count( $totals ) ); + $this->assertEquals( 4, count( $totals ) ); $this->assertArrayHasKey( 'amount', $totals ); $this->assertArrayHasKey( 'coupons_count', $totals ); $this->assertArrayHasKey( 'orders_count', $totals ); + $this->assertArrayHasKey( 'segments', $totals ); $intervals = $properties['intervals']['items']['properties']; $this->assertEquals( 6, count( $intervals ) ); @@ -165,9 +168,11 @@ class WC_Tests_API_Reports_Coupons_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'subtotals', $intervals ); $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; - $this->assertEquals( 3, count( $subtotals ) ); - $this->assertArrayHasKey( 'amount', $totals ); - $this->assertArrayHasKey( 'coupons_count', $totals ); - $this->assertArrayHasKey( 'orders_count', $totals ); + $this->assertEquals( 4, count( $subtotals ) ); + $this->assertArrayHasKey( 'amount', $subtotals ); + $this->assertArrayHasKey( 'coupons_count', $subtotals ); + $this->assertArrayHasKey( 'orders_count', $subtotals ); + $this->assertArrayHasKey( 'segments', $subtotals ); + } } diff --git a/plugins/woocommerce-admin/tests/api/reports-orders-stats.php b/plugins/woocommerce-admin/tests/api/reports-orders-stats.php index 6a8f34389e1..16d9c4fd3a2 100644 --- a/plugins/woocommerce-admin/tests/api/reports-orders-stats.php +++ b/plugins/woocommerce-admin/tests/api/reports-orders-stats.php @@ -3,7 +3,10 @@ * Reports Orders Stats REST API Test * * @package WooCommerce\Tests\API - * @since 3.5.0 + */ + +/** + * WC_Tests_API_Reports_Orders_Stats */ /** @@ -90,11 +93,17 @@ class WC_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'intervals', $properties ); $totals = $properties['totals']['properties']; - $this->assertEquals( 4, count( $totals ) ); + $this->assertEquals( 10, count( $totals ) ); $this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'avg_order_value', $totals ); $this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'avg_items_per_order', $totals ); + $this->assertArrayHasKey( 'num_items_sold', $totals ); + $this->assertArrayHasKey( 'coupons', $totals ); + $this->assertArrayHasKey( 'num_returning_customers', $totals ); + $this->assertArrayHasKey( 'num_new_customers', $totals ); + $this->assertArrayHasKey( 'products', $totals ); + $this->assertArrayHasKey( 'segments', $totals ); $intervals = $properties['intervals']['items']['properties']; $this->assertEquals( 6, count( $intervals ) ); @@ -106,10 +115,15 @@ class WC_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'subtotals', $intervals ); $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; - $this->assertEquals( 4, count( $subtotals ) ); - $this->assertArrayHasKey( 'net_revenue', $totals ); - $this->assertArrayHasKey( 'avg_order_value', $totals ); - $this->assertArrayHasKey( 'orders_count', $totals ); - $this->assertArrayHasKey( 'avg_items_per_order', $totals ); + $this->assertEquals( 9, count( $subtotals ) ); + $this->assertArrayHasKey( 'net_revenue', $subtotals ); + $this->assertArrayHasKey( 'avg_order_value', $subtotals ); + $this->assertArrayHasKey( 'orders_count', $subtotals ); + $this->assertArrayHasKey( 'avg_items_per_order', $subtotals ); + $this->assertArrayHasKey( 'num_items_sold', $subtotals ); + $this->assertArrayHasKey( 'coupons', $subtotals ); + $this->assertArrayHasKey( 'num_returning_customers', $subtotals ); + $this->assertArrayHasKey( 'num_new_customers', $subtotals ); + $this->assertArrayHasKey( 'segments', $subtotals ); } } diff --git a/plugins/woocommerce-admin/tests/api/reports-products-stats.php b/plugins/woocommerce-admin/tests/api/reports-products-stats.php index c4cde5009c9..29825af677d 100644 --- a/plugins/woocommerce-admin/tests/api/reports-products-stats.php +++ b/plugins/woocommerce-admin/tests/api/reports-products-stats.php @@ -89,6 +89,7 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case { 'net_revenue' => 100.0, 'orders_count' => 1, 'products_count' => 1, + 'segments' => array(), ), 'intervals' => array( array( @@ -102,6 +103,7 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case { 'net_revenue' => 100.0, 'orders_count' => 1, 'products_count' => 1, + 'segments' => array(), ), ), ), @@ -140,10 +142,11 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'intervals', $properties ); $totals = $properties['totals']['properties']; - $this->assertEquals( 3, count( $totals ) ); + $this->assertEquals( 4, count( $totals ) ); $this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'items_sold', $totals ); $this->assertArrayHasKey( 'orders_count', $totals ); + $this->assertArrayHasKey( 'segments', $totals ); $intervals = $properties['intervals']['items']['properties']; $this->assertEquals( 6, count( $intervals ) ); @@ -155,9 +158,10 @@ class WC_Tests_API_Reports_Products_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'subtotals', $intervals ); $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; - $this->assertEquals( 3, count( $subtotals ) ); + $this->assertEquals( 4, count( $subtotals ) ); $this->assertArrayHasKey( 'net_revenue', $subtotals ); $this->assertArrayHasKey( 'items_sold', $subtotals ); $this->assertArrayHasKey( 'orders_count', $subtotals ); + $this->assertArrayHasKey( 'segments', $subtotals ); } } diff --git a/plugins/woocommerce-admin/tests/api/reports-revenue-stats.php b/plugins/woocommerce-admin/tests/api/reports-revenue-stats.php index 982ca4df612..51511e7a2e6 100644 --- a/plugins/woocommerce-admin/tests/api/reports-revenue-stats.php +++ b/plugins/woocommerce-admin/tests/api/reports-revenue-stats.php @@ -97,7 +97,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'intervals', $properties ); $totals = $properties['totals']['properties']; - $this->assertEquals( 9, count( $totals ) ); + $this->assertEquals( 10, count( $totals ) ); $this->assertArrayHasKey( 'gross_revenue', $totals ); $this->assertArrayHasKey( 'net_revenue', $totals ); $this->assertArrayHasKey( 'coupons', $totals ); @@ -107,6 +107,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'num_items_sold', $totals ); $this->assertArrayHasKey( 'products', $totals ); + $this->assertArrayHasKey( 'segments', $totals ); $intervals = $properties['intervals']['items']['properties']; $this->assertEquals( 6, count( $intervals ) ); @@ -118,7 +119,7 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'subtotals', $intervals ); $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; - $this->assertEquals( 8, count( $subtotals ) ); + $this->assertEquals( 9, count( $subtotals ) ); $this->assertArrayHasKey( 'gross_revenue', $subtotals ); $this->assertArrayHasKey( 'net_revenue', $subtotals ); $this->assertArrayHasKey( 'coupons', $subtotals ); @@ -127,5 +128,6 @@ class WC_Tests_API_Reports_Revenue_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'refunds', $subtotals ); $this->assertArrayHasKey( 'orders_count', $subtotals ); $this->assertArrayHasKey( 'num_items_sold', $subtotals ); + $this->assertArrayHasKey( 'segments', $subtotals ); } } diff --git a/plugins/woocommerce-admin/tests/api/reports-taxes-stats.php b/plugins/woocommerce-admin/tests/api/reports-taxes-stats.php index 4b09aba7023..9555f8bb56c 100644 --- a/plugins/woocommerce-admin/tests/api/reports-taxes-stats.php +++ b/plugins/woocommerce-admin/tests/api/reports-taxes-stats.php @@ -143,12 +143,13 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case { $totals = $properties['totals']['properties']; - $this->assertEquals( 5, count( $totals ) ); + $this->assertEquals( 6, count( $totals ) ); $this->assertArrayHasKey( 'order_tax', $totals ); $this->assertArrayHasKey( 'orders_count', $totals ); $this->assertArrayHasKey( 'shipping_tax', $totals ); $this->assertArrayHasKey( 'tax_codes', $totals ); $this->assertArrayHasKey( 'total_tax', $totals ); + $this->assertArrayHasKey( 'segments', $totals ); $intervals = $properties['intervals']['items']['properties']; $this->assertEquals( 6, count( $intervals ) ); @@ -160,12 +161,13 @@ class WC_Tests_API_Reports_Taxes_Stats extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'subtotals', $intervals ); $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; - $this->assertEquals( 5, count( $subtotals ) ); - $this->assertArrayHasKey( 'order_tax', $totals ); - $this->assertArrayHasKey( 'orders_count', $totals ); - $this->assertArrayHasKey( 'shipping_tax', $totals ); - $this->assertArrayHasKey( 'tax_codes', $totals ); - $this->assertArrayHasKey( 'total_tax', $totals ); + $this->assertEquals( 6, count( $subtotals ) ); + $this->assertArrayHasKey( 'order_tax', $subtotals ); + $this->assertArrayHasKey( 'orders_count', $subtotals ); + $this->assertArrayHasKey( 'shipping_tax', $subtotals ); + $this->assertArrayHasKey( 'tax_codes', $subtotals ); + $this->assertArrayHasKey( 'total_tax', $subtotals ); + $this->assertArrayHasKey( 'segments', $subtotals ); } } diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-coupons-stats.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-coupons-stats.php index f9c6cc201f6..e130bfb0c6a 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-coupons-stats.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-coupons-stats.php @@ -72,9 +72,10 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case { 'pages' => 1, 'page_no' => 1, 'totals' => (object) array( - 'amount' => 2 * $coupon_1_amount + $coupon_2_amount, + 'amount' => floatval( 2 * $coupon_1_amount + $coupon_2_amount ), 'coupons_count' => 2, 'orders_count' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -84,9 +85,10 @@ class WC_Tests_Reports_Coupons_Stats extends WC_Unit_Test_Case { 'date_end' => $end_datetime->format( 'Y-m-d H:i:s' ), 'date_end_gmt' => $end_datetime->format( 'Y-m-d H:i:s' ), 'subtotals' => (object) array( - 'amount' => 2 * $coupon_1_amount + $coupon_2_amount, + 'amount' => floatval( 2 * $coupon_1_amount + $coupon_2_amount ), 'coupons_count' => 2, 'orders_count' => 2, + 'segments' => array(), ), ), ), diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php index 0c594768993..9a0edb16df8 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders-stats.php @@ -66,6 +66,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 1, 'products' => 1, + 'segments' => array(), ), 'intervals' => array( array( @@ -87,6 +88,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => 68, 'num_returning_customers' => 0, 'num_new_customers' => 1, + 'segments' => array(), ), ), ), @@ -111,6 +113,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 1, 'products' => '1', + 'segments' => array(), ), 'intervals' => array( array( @@ -128,6 +131,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'coupons' => 20, 'num_returning_customers' => 0, 'num_new_customers' => 1, + 'segments' => array(), ), ), ), @@ -382,6 +386,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -403,6 +408,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -469,6 +475,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -490,6 +497,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -542,6 +550,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -563,6 +572,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -598,6 +608,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 0, 'products' => 0, + 'segments' => array(), ), 'intervals' => array( array( @@ -619,6 +630,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => 0, 'num_returning_customers' => 0, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -675,6 +687,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -696,6 +709,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -748,6 +762,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 3, + 'segments' => array(), ), 'intervals' => array( array( @@ -769,6 +784,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -817,6 +833,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 2, + 'segments' => array(), // product 3 and product 4 (that is sometimes included in the orders with product 3). ), 'intervals' => array( @@ -839,6 +856,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -889,6 +907,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 3, + 'segments' => array(), ), 'intervals' => array( array( @@ -910,6 +929,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -959,6 +979,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -980,6 +1001,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -1032,6 +1054,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1053,6 +1076,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -1107,6 +1131,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1128,6 +1153,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -1179,6 +1205,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1200,6 +1227,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -1251,6 +1279,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1272,6 +1301,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1325,6 +1355,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1346,6 +1377,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1402,6 +1434,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1423,6 +1456,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1470,6 +1504,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1491,6 +1526,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1541,6 +1577,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 1, 'num_new_customers' => 0, 'products' => 1, + 'segments' => array(), ), 'intervals' => array( array( @@ -1562,6 +1599,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 1, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1616,6 +1654,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1637,6 +1676,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1692,6 +1732,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1713,6 +1754,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1764,6 +1806,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1785,6 +1828,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1839,6 +1883,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1860,6 +1905,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1918,6 +1964,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1939,6 +1986,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1998,6 +2046,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_new_customers' => $new_customers, // Prod_1, status_1, no coupon orders included here, so 2 new cust orders. 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -2019,6 +2068,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2080,6 +2130,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -2101,6 +2152,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2166,6 +2218,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -2187,6 +2240,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2244,6 +2298,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2265,6 +2320,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2321,6 +2377,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2342,6 +2399,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2398,6 +2456,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2419,6 +2478,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2475,6 +2535,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2496,6 +2557,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2552,6 +2614,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2573,6 +2636,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2632,6 +2696,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2653,6 +2718,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2715,6 +2781,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2736,6 +2803,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2798,6 +2866,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2819,6 +2888,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2884,6 +2954,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2905,6 +2976,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2973,6 +3045,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2994,6 +3067,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -3005,4 +3079,510 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { } + /** + * Test segmenting by product id and by variation id. + */ + public function test_segmenting_by_product_and_variation() { + // Simple product. + $product_1_price = 25; + $product_1 = new WC_Product_Simple(); + $product_1->set_name( 'Simple Product' ); + $product_1->set_regular_price( $product_1_price ); + $product_1->save(); + + // Variable product. + $product_2 = new WC_Product_Variable(); + $product_2->set_name( 'Variable Product' ); + $product_2->save(); + + $child_1 = new WC_Product_Variation(); + $child_1->set_parent_id( $product_2->get_id() ); + $child_1->set_regular_price( 23 ); + $child_1->save(); + + $child_2 = new WC_Product_Variation(); + $child_2->set_parent_id( $product_2->get_id() ); + $child_2->set_regular_price( 27 ); + $child_2->save(); + + $product_2->set_children( array( $child_1->get_id(), $child_2->get_id() ) ); + + $child_1->set_stock_status( 'instock' ); + $child_1->save(); + $child_2->set_stock_status( 'instock' ); + $child_2->save(); + WC_Product_Variable::sync( $product_2 ); + + // Simple product, not used. + $product_3_price = 17; + $product_3 = new WC_Product_Simple(); + $product_3->set_name( 'Simple Product not used' ); + $product_3->set_regular_price( $product_3_price ); + $product_3->save(); + + $order_status = 'completed'; + + $customer_1 = WC_Helper_Customer::create_customer( 'cust_1', 'pwd_1', 'user_1@mail.com' ); + + $order_1_time = time(); + $order_3_time = $order_1_time - 1 * HOUR_IN_SECONDS; + + // Order 3: 4 x product 1, done one hour earlier. + $order_3 = WC_Helper_Order::create_order( $customer_1->get_id(), $product_1 ); + $order_3->set_date_created( $order_3_time ); + $order_3->set_status( $order_status ); + $order_3->calculate_totals(); + $order_3->save(); + + // Order 1: 4 x product 1 & 3 x product 2-child 1. + $order_1 = WC_Helper_Order::create_order( $customer_1->get_id(), $product_1 ); + $item = new WC_Order_Item_Product(); + + $item->set_props( + array( + 'product_id' => $product_2->get_id(), + 'variation_id' => $child_1->get_id(), + 'quantity' => 3, + 'subtotal' => 3 * floatval( $child_1->get_price() ), + 'total' => 3 * floatval( $child_1->get_price() ), + ) + ); + $item->save(); + $order_1->add_item( $item ); + $order_1->set_status( $order_status ); + $order_1->calculate_totals(); + $order_1->save(); + + // Order 2: 4 x product 2-child 1 & 1 x product 2-child 2. + $order_2 = WC_Helper_Order::create_order( $customer_1->get_id(), $child_1 ); + $item = new WC_Order_Item_Product(); + $item->set_props( + array( + 'product_id' => $product_2->get_id(), + 'variation_id' => $child_2->get_id(), + 'quantity' => 1, + 'subtotal' => floatval( $child_2->get_price() ), + 'total' => floatval( $child_2->get_price() ), + ) + ); + $item->save(); + $order_2->add_item( $item ); + $order_2->set_status( $order_status ); + $order_2->calculate_totals(); + $order_2->save(); + + $data_store = new WC_Admin_Reports_Orders_Stats_Data_Store(); + + // Tests for before & after set to current hour. + $now = new DateTime(); + + $two_hours_back = new DateTime(); + $i1_start_timestamp = $order_1_time - 2 * HOUR_IN_SECONDS; + $two_hours_back->setTimestamp( $i1_start_timestamp ); + $i1_end_timestamp = $i1_start_timestamp + ( 3600 - ( $i1_start_timestamp % 3600 ) ) - 1; + $i1_start = new DateTime(); + $i1_start->setTimestamp( $i1_start_timestamp ); + $i1_end = new DateTime(); + $i1_end->setTimestamp( $i1_end_timestamp ); + + $i2_start_timestamp = $i1_end_timestamp + 1; + $i2_end_timestamp = $i1_end_timestamp + 3600; + $i2_start = new DateTime(); + $i2_start->setTimestamp( $i2_start_timestamp ); + $i2_end = new DateTime(); + $i2_end->setTimestamp( $i2_end_timestamp ); + + $i3_start_timestamp = $i2_end_timestamp + 1; + $i3_end_timestamp = $now->format( 'U' ); + $i3_start = new DateTime(); + $i3_start->setTimestamp( $i3_start_timestamp ); + $i3_end = new DateTime(); + $i3_end->setTimestamp( $i3_end_timestamp ); + + $query_args = array( + 'after' => $two_hours_back->format( WC_Admin_Reports_Interval::$sql_datetime_format ), + 'before' => $now->format( WC_Admin_Reports_Interval::$sql_datetime_format ), + 'interval' => 'hour', + 'segmentby' => 'product', + ); + + $shipping_amnt = 10; + $o1_net_revenue = 4 * $product_1_price + 3 * intval( $child_1->get_price() ); + $o2_net_revenue = 4 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() ); + $o3_net_revenue = 4 * $product_1_price; + $o1_num_items = 4 + 3; + $o2_num_items = 4 + 1; + $o3_num_items = 4; + + // Totals. + $orders_count = 3; + $num_items_sold = 7 + 5 + 4; + $shipping = $orders_count * $shipping_amnt; + $net_revenue = $o1_net_revenue + $o2_net_revenue + $o3_net_revenue; + $gross_revenue = $net_revenue + $shipping; + $new_customers = 1; + // Totals segments. + $p1_orders_count = 2; + $p1_num_items_sold = 8; + $p1_shipping = round( $shipping_amnt / $o1_num_items * 4, 6 ) + round( $shipping_amnt / $o3_num_items * 4, 6 ); + $p1_net_revenue = 8 * $product_1_price; + $p1_gross_revenue = $p1_net_revenue + $p1_shipping; + $p1_new_customers = 1; + + $p2_orders_count = 2; + $p2_num_items_sold = 8; + $p2_shipping = round( $shipping_amnt / $o1_num_items * 3, 6 ) + $shipping_amnt; + $p2_net_revenue = 7 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() ); + $p2_gross_revenue = $p2_net_revenue + $p2_shipping; + $p2_new_customers = 0; + + // Interval 3. + // I3 Subtotals. + $i3_tot_orders_count = 2; + $i3_tot_num_items_sold = 4 + 3 + 4 + 1; + $i3_tot_shipping = $i3_tot_orders_count * $shipping_amnt; + $i3_tot_net_revenue = 4 * $product_1_price + 7 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() ); + $i3_tot_gross_revenue = $i3_tot_net_revenue + $i3_tot_shipping; + $i3_tot_new_customers = 0; + + // I3 Segments. + $i3_p1_orders_count = 1; + $i3_p1_num_items_sold = 4; + $i3_p1_shipping = round( $shipping_amnt / $o1_num_items * 4, 6 ); + $i3_p1_net_revenue = $i3_p1_num_items_sold * $product_1_price; + $i3_p1_gross_revenue = $i3_p1_net_revenue + $i3_p1_shipping; + $i3_p1_new_customers = 0; + + $i3_p2_orders_count = 2; + $i3_p2_num_items_sold = 8; + $i3_p2_shipping = round( $shipping_amnt / $o1_num_items * 3, 6 ) + $shipping_amnt; + $i3_p2_net_revenue = 7 * intval( $child_1->get_price() ) + 1 * intval( $child_2->get_price() ); + $i3_p2_gross_revenue = $i3_p2_net_revenue + $i3_p2_shipping; + $i3_p2_new_customers = 0; + + // Interval 2 + // I2 Subtotals. + $i2_tot_orders_count = 1; + $i2_tot_num_items_sold = 4; + $i2_tot_shipping = $i2_tot_orders_count * $shipping_amnt; + $i2_tot_net_revenue = 4 * $product_1_price; + $i2_tot_gross_revenue = $i2_tot_net_revenue + $i2_tot_shipping; + $i2_tot_new_customers = 1; + + // I2 Segments. + $i2_p1_orders_count = 1; + $i2_p1_num_items_sold = 4; + $i2_p1_shipping = $shipping_amnt; + $i2_p1_net_revenue = 4 * $product_1_price; + $i2_p1_gross_revenue = $i2_p1_net_revenue + $i2_p1_shipping; + $i2_p1_new_customers = 1; + + $i2_p2_orders_count = 0; + $i2_p2_num_items_sold = 0; + $i2_p2_shipping = 0; + $i2_p2_net_revenue = 0; + $i2_p2_gross_revenue = $i2_p2_net_revenue + $i2_p2_shipping; + $i2_p2_new_customers = 0; + + $expected_stats = array( + 'totals' => array( + 'orders_count' => $orders_count, + 'num_items_sold' => $num_items_sold, + 'gross_revenue' => $gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $shipping, + 'net_revenue' => $net_revenue, + 'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ), + 'avg_order_value' => $net_revenue / $orders_count, + 'num_returning_customers' => 0, + 'num_new_customers' => $new_customers, + 'products' => 2, + 'segments' => array( + array( + 'segment_id' => $product_1->get_id(), + 'subtotals' => array( + 'orders_count' => $p1_orders_count, + 'num_items_sold' => $p1_num_items_sold, + 'gross_revenue' => $p1_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $p1_shipping, + 'net_revenue' => $p1_net_revenue, + 'avg_items_per_order' => ( $o1_num_items + $o3_num_items ) / $p1_orders_count, + 'avg_order_value' => ( $o1_net_revenue + $o3_net_revenue ) / $p1_orders_count, + 'num_returning_customers' => $p1_orders_count - $p1_new_customers, + 'num_new_customers' => $p1_new_customers, + ), + ), + array( + 'segment_id' => $product_2->get_id(), + 'subtotals' => array( + 'orders_count' => $p2_orders_count, + 'num_items_sold' => $p2_num_items_sold, + 'gross_revenue' => $p2_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $p2_shipping, + 'net_revenue' => $p2_net_revenue, + 'avg_items_per_order' => ( $o1_num_items + $o2_num_items ) / $p2_orders_count, + 'avg_order_value' => ( $o1_net_revenue + $o2_net_revenue ) / $p2_orders_count, + 'num_returning_customers' => $p2_orders_count - $p2_new_customers, + 'num_new_customers' => $p2_new_customers, + ), + ), + array( + 'segment_id' => $product_3->get_id(), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + ), + ), + ), + ), + 'intervals' => array( + array( + 'interval' => $i3_start->format( 'Y-m-d H' ), + 'date_start' => $i3_start->format( 'Y-m-d H:i:s' ), + 'date_start_gmt' => $i3_start->format( 'Y-m-d H:i:s' ), + 'date_end' => $i3_end->format( 'Y-m-d H:i:s' ), + 'date_end_gmt' => $i3_end->format( 'Y-m-d H:i:s' ), + 'subtotals' => array( + 'orders_count' => $i3_tot_orders_count, + 'num_items_sold' => $i3_tot_num_items_sold, + 'gross_revenue' => $i3_tot_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $i3_tot_shipping, + 'net_revenue' => $i3_tot_net_revenue, + 'avg_items_per_order' => $i3_tot_num_items_sold / $i3_tot_orders_count, + 'avg_order_value' => $i3_tot_net_revenue / $i3_tot_orders_count, + 'num_returning_customers' => 1, + 'num_new_customers' => $i3_tot_new_customers, + 'segments' => array( + array( + 'segment_id' => $product_1->get_id(), + 'subtotals' => array( + 'orders_count' => $i3_p1_orders_count, + 'num_items_sold' => $i3_p1_num_items_sold, + 'gross_revenue' => $i3_p1_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $i3_p1_shipping, + 'net_revenue' => $i3_p1_net_revenue, + 'avg_items_per_order' => $o1_num_items / $i3_p1_orders_count, + 'avg_order_value' => $o1_net_revenue / $i3_p1_orders_count, + 'num_returning_customers' => $i3_p1_orders_count - $i3_p1_new_customers, + 'num_new_customers' => $i3_p1_new_customers, + ), + ), + array( + 'segment_id' => $product_2->get_id(), + 'subtotals' => array( + 'orders_count' => $i3_p2_orders_count, + 'num_items_sold' => $i3_p2_num_items_sold, + 'gross_revenue' => $i3_p2_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $i3_p2_shipping, + 'net_revenue' => $i3_p2_net_revenue, + 'avg_items_per_order' => ( $o1_num_items + $o2_num_items ) / $i3_p2_orders_count, + 'avg_order_value' => ( $o1_net_revenue + $o2_net_revenue ) / $i3_p2_orders_count, + 'num_returning_customers' => $i3_p2_orders_count - $i3_p2_new_customers, + 'num_new_customers' => $i3_p2_new_customers, + ), + ), + array( + 'segment_id' => $product_3->get_id(), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + ), + ), + ), + ), + ), + array( + 'interval' => $i2_start->format( 'Y-m-d H' ), + 'date_start' => $i2_start->format( 'Y-m-d H:i:s' ), + 'date_start_gmt' => $i2_start->format( 'Y-m-d H:i:s' ), + 'date_end' => $i2_end->format( 'Y-m-d H:i:s' ), + 'date_end_gmt' => $i2_end->format( 'Y-m-d H:i:s' ), + 'subtotals' => array( + 'orders_count' => $i2_tot_orders_count, + 'num_items_sold' => $i2_tot_num_items_sold, + 'gross_revenue' => $i2_tot_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $i2_tot_shipping, + 'net_revenue' => $i2_tot_net_revenue, + 'avg_items_per_order' => $i2_tot_num_items_sold / $i2_tot_orders_count, + 'avg_order_value' => $i2_tot_net_revenue / $i2_tot_orders_count, + 'num_returning_customers' => $i2_tot_orders_count - $i2_tot_new_customers, + 'num_new_customers' => $i2_tot_new_customers, + 'segments' => array( + array( + 'segment_id' => $product_1->get_id(), + 'subtotals' => array( + 'orders_count' => $i2_p1_orders_count, + 'num_items_sold' => $i2_p1_num_items_sold, + 'gross_revenue' => $i2_p1_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $i2_p1_shipping, + 'net_revenue' => $i2_p1_net_revenue, + 'avg_items_per_order' => $o3_num_items / $i2_p1_orders_count, + 'avg_order_value' => $o3_net_revenue / $i2_p1_orders_count, + 'num_returning_customers' => $i2_p1_orders_count - $i2_p1_new_customers, + 'num_new_customers' => $i2_p1_new_customers, + ), + ), + array( + 'segment_id' => $product_2->get_id(), + 'subtotals' => array( + 'orders_count' => $i2_p2_orders_count, + 'num_items_sold' => $i2_p2_num_items_sold, + 'gross_revenue' => $i2_p2_gross_revenue, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => $i2_p2_shipping, + 'net_revenue' => $i2_p2_net_revenue, + 'avg_items_per_order' => $i2_p2_orders_count ? $o3_num_items / $i2_p2_orders_count : 0, + 'avg_order_value' => $i2_p2_orders_count ? $o3_net_revenue / $i2_p2_orders_count : 0, + 'num_returning_customers' => $i2_p2_orders_count - $i2_p2_new_customers, + 'num_new_customers' => $i2_p2_new_customers, + ), + ), + array( + 'segment_id' => $product_3->get_id(), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + ), + ), + ), + ), + ), + array( + 'interval' => $i1_start->format( 'Y-m-d H' ), + 'date_start' => $i1_start->format( 'Y-m-d H:i:s' ), + 'date_start_gmt' => $i1_start->format( 'Y-m-d H:i:s' ), + 'date_end' => $i1_end->format( 'Y-m-d H:i:s' ), + 'date_end_gmt' => $i1_end->format( 'Y-m-d H:i:s' ), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + 'segments' => array( + array( + 'segment_id' => $product_1->get_id(), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + ), + ), + array( + 'segment_id' => $product_2->get_id(), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + ), + ), + array( + 'segment_id' => $product_3->get_id(), + 'subtotals' => array( + 'orders_count' => 0, + 'num_items_sold' => 0, + 'gross_revenue' => 0, + 'coupons' => 0, + 'refunds' => 0, + 'taxes' => 0, + 'shipping' => 0, + 'net_revenue' => 0, + 'avg_items_per_order' => 0, + 'avg_order_value' => 0, + 'num_returning_customers' => 0, + 'num_new_customers' => 0, + ), + ), + ), + ), + ), + ), + 'total' => 3, + 'pages' => 1, + 'page_no' => 1, + ); + $actual = json_decode( json_encode( $data_store->get_data( $query_args ) ), true ); + $this->assertEquals( $expected_stats, $actual, 'Segmenting by product, expected: ' . print_r( $expected_stats, true ) . '; actual: ' . print_r( $actual, true ) ); + } + } 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 359dd0243df..fa74b3b9d22 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 @@ -62,7 +62,8 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { 'avg_order_value' => 80, 'num_returning_customers' => 0, 'num_new_customers' => 1, - 'products' => '1', + 'products' => 1, + 'segments' => array(), ), 'intervals' => array( array( @@ -84,6 +85,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { 'avg_order_value' => 80, 'num_returning_customers' => 0, 'num_new_customers' => 1, + 'segments' => array(), ), ), ), @@ -107,6 +109,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { 'shipping' => 10, 'net_revenue' => 80, 'products' => '1', + 'segments' => array(), ), 'intervals' => array( array( @@ -124,6 +127,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { 'taxes' => 7, 'shipping' => 10, 'net_revenue' => 80, + 'segments' => array(), ), ), ),