From 4d6ea24e9e2d36468ea5914eb8f460f6deca93bd Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Thu, 20 Dec 2018 12:28:29 +0100 Subject: [PATCH 01/91] Db update for product lookup table to enable segmenting. --- .../includes/class-wc-admin-api-init.php | 6 ++ .../includes/wc-admin-order-functions.php | 82 +++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 95c2bd2d242..2a33aece4e4 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -395,6 +395,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, + price 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, PRIMARY KEY (order_item_id), KEY order_id (order_id), KEY product_id (product_id), diff --git a/plugins/woocommerce-admin/includes/wc-admin-order-functions.php b/plugins/woocommerce-admin/includes/wc-admin-order-functions.php index c1dd7981545..987ed683a0c 100644 --- a/plugins/woocommerce-admin/includes/wc-admin-order-functions.php +++ b/plugins/woocommerce-admin/includes/wc-admin-order-functions.php @@ -21,17 +21,77 @@ function wc_admin_order_product_lookup_entry( $order_id ) { } foreach ( $order->get_items() as $order_item ) { + // Shipping amount based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php + // distributed simply based on number of line items. + $order_items = $order->get_item_count(); + $refunded = $order->get_total_shipping_refunded(); + if ( $refunded > 0 ) { + $total_shipping_amount = $order->get_shipping_total() - $refunded; + } else { + $total_shipping_amount = $order->get_shipping_total(); + } + $shipping_amount = $total_shipping_amount / $order_items; + + // Shipping amount tax based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php + // distribute simply based on number of line items. + $shipping_tax_amount = 0; + // TODO: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? + if ( wc_tax_enabled() ) { + $order_taxes = $order->get_taxes(); + $line_items_shipping = $order->get_items( 'shipping' ); + $total_shipping_tax_amount = 0; + foreach ( $line_items_shipping as $item_id => $item ) { + $tax_data = $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 = $order->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; + } + } + } + } + $shipping_tax_amount = $total_shipping_tax_amount / $order_items; + } + + // Tax amount. + // TODO: check if this calculates tax correctly with refunds. + $tax_amount = 0; + if ( wc_tax_enabled() ) { + $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; + } + } + + $net_revenue = $order_item->get_subtotal( 'edit' ); + + // Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php. + $coupon_amount = $order_item->get_subtotal( 'edit' ) - $order_item->get_total( 'edit' ); + $wpdb->replace( $wpdb->prefix . 'wc_order_product_lookup', array( - 'order_item_id' => $order_item->get_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' ), - 'product_net_revenue' => $order_item->get_subtotal( 'edit' ), - 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'order_item_id' => $order_item->get_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' ), + 'product_net_revenue' => $net_revenue, + 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + 'price' => $order_item->get_subtotal( 'edit' ) / $order_item->get_quantity( 'edit' ), + 'coupon_amount' => $coupon_amount, + 'tax_amount' => $tax_amount, + 'shipping_amount' => $shipping_amount, + 'shipping_tax_amount' => $shipping_tax_amount, + 'product_gross_revenue' => $net_revenue + $tax_amount + $shipping_amount + $shipping_tax_amount, ), array( '%d', @@ -42,6 +102,12 @@ function wc_admin_order_product_lookup_entry( $order_id ) { '%d', '%f', '%s', + '%f', + '%f', + '%f', + '%f', + '%f', + '%f', ) ); } From 999c7fe8caf87aa8d7cd25ddc13a75dcb46c5ef2 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Thu, 20 Dec 2018 12:35:45 +0100 Subject: [PATCH 02/91] Updated REST API controller for segmenting, added schema changes missed before. --- ...n-rest-reports-orders-stats-controller.php | 92 +++++++++++++++++-- 1 file changed, 82 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-orders-stats-controller.php index 592f4ec1290..5e6617e4aa0 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 @@ -52,10 +52,11 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont $args['status_is_not'] = (array) $request['status_is_not']; $args['product_includes'] = (array) $request['product_includes']; $args['product_excludes'] = (array) $request['product_excludes']; - $args['coupon_includes'] = (array) $request['coupon_includes']; - $args['coupon_excludes'] = (array) $request['coupon_excludes']; + $args['coupon_includes'] = (array) $request['coupon_includes']; + $args['coupon_excludes'] = (array) $request['coupon_excludes']; $args['customer'] = $request['customer']; $args['categories'] = (array) $request['categories']; + $args['segmentby'] = $request['segmentby']; return $args; } @@ -140,33 +141,90 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont * @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, ), - 'avg_order_value' => array( + 'avg_order_value' => array( 'description' => __( 'Average order value.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'orders_count' => array( + 'orders_count' => array( 'description' => __( 'Amount of orders', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - '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, ), + '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' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'enum' => array( 'day', 'week', 'month', 'year' ), + ), + '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'] ); + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'report_orders_stats', @@ -223,7 +281,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, - 'properties' => $totals, + 'properties' => $data_values, ), ), ), @@ -353,7 +411,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont '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( @@ -362,7 +420,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont '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( @@ -380,6 +438,20 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont ), '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', + 'tax', + 'coupon', + 'billing_country', // just to test out if it's possible. + 'customer_type', // new vs returning. + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } From 1931f3bf4876a3463d05558b570caf681478f585 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 2 Jan 2019 11:31:48 +0100 Subject: [PATCH 03/91] Added table name to time limit clauses. --- .../class-wc-admin-reports-interval.php | 17 +++++++++-------- ...c-admin-reports-coupons-stats-data-store.php | 2 +- .../class-wc-admin-reports-data-store.php | 17 +++++++++-------- 3 files changed, 19 insertions(+), 17 deletions(-) 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 f1765f257b2..936186294dc 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -30,28 +30,29 @@ class WC_Admin_Reports_Interval { * Returns date format to be used as grouping clause in SQL. * * @param string $time_interval Time interval. + * @param string $table_name Name of db table. * @return mixed */ - public static function db_datetime_format( $time_interval ) { + public static function db_datetime_format( $time_interval, $table_name ) { $first_day_of_week = absint( get_option( 'start_of_week' ) ); if ( 1 === $first_day_of_week ) { // Week begins on Monday, ISO 8601. - $week_format = "DATE_FORMAT(date_created, '%x-%v')"; + $week_format = "DATE_FORMAT($table_name.date_created, '%x-%v')"; } else { // Week begins on day other than specified by ISO 8601, needs to be in sync with function simple_week_number. - $week_format = "CONCAT(YEAR(date_created), '-', LPAD( FLOOR( ( DAYOFYEAR(date_created) + ( ( DATE_FORMAT(MAKEDATE(YEAR(date_created),1), '%w') - $first_day_of_week + 7 ) % 7 ) - 1 ) / 7 ) + 1 , 2, '0'))"; + $week_format = "CONCAT(YEAR($table_name.date_created), '-', LPAD( FLOOR( ( DAYOFYEAR($table_name.date_created) + ( ( DATE_FORMAT(MAKEDATE(YEAR($table_name.date_created),1), '%w') - $first_day_of_week + 7 ) % 7 ) - 1 ) / 7 ) + 1 , 2, '0'))"; } // Whenever this is changed, double check method time_interval_id to make sure they are in sync. $mysql_date_format_mapping = array( - 'hour' => "DATE_FORMAT(date_created, '%Y-%m-%d %H')", - 'day' => "DATE_FORMAT(date_created, '%Y-%m-%d')", + 'hour' => "DATE_FORMAT($table_name.date_created, '%Y-%m-%d %H')", + 'day' => "DATE_FORMAT($table_name.date_created, '%Y-%m-%d')", 'week' => $week_format, - 'month' => "DATE_FORMAT(date_created, '%Y-%m')", - 'quarter' => "CONCAT(YEAR(date_created), '-', QUARTER(date_created))", - 'year' => 'YEAR(date_created)', + 'month' => "DATE_FORMAT($table_name.date_created, '%Y-%m')", + 'quarter' => "CONCAT(YEAR($table_name.date_created), '-', QUARTER($table_name.date_created))", + 'year' => "YEAR($table_name.date_created)", ); 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 ba42223ca42..885d5ce8a01 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 @@ -160,7 +160,7 @@ class WC_Admin_Reports_Coupons_Stats_Data_Store extends WC_Admin_Reports_Coupons $totals = (object) $this->cast_numbers( $totals[0] ); // Intervals. - $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count ); + $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name ); if ( '' !== $selections ) { $selections = ', ' . $selections; 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 a4b2e34ab1d..7060882f342 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 @@ -186,12 +186,13 @@ class WC_Admin_Reports_Data_Store { * If there are less records in the database than time intervals, then we need to remap offset in SQL query * to fetch correct records. * - * @param array $intervals_query Array with clauses for the Intervals SQL query. - * @param array $query_args Query arguements. - * @param int $db_interval_count Database interval count. - * @param int $expected_interval_count Expected interval count on the output. + * @param array $intervals_query Array with clauses for the Intervals SQL query. + * @param array $query_args Query arguements. + * @param int $db_interval_count Database interval count. + * @param int $expected_interval_count Expected interval count on the output. + * @param string $table_name Name of SQL table where the date_created column (used for time filtering) exists. */ - protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count ) { + protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count, $table_name ) { if ( $db_interval_count === $expected_interval_count ) { return; } @@ -263,8 +264,8 @@ class WC_Admin_Reports_Data_Store { $query_args['adj_after'] = $new_start_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); $query_args['adj_before'] = $new_end_date->format( WC_Admin_Reports_Interval::$iso_datetime_format ); $intervals_query['where_time_clause'] = ''; - $intervals_query['where_time_clause'] .= " AND date_created <= '{$query_args['adj_before']}'"; - $intervals_query['where_time_clause'] .= " AND date_created >= '{$query_args['adj_after']}'"; + $intervals_query['where_time_clause'] .= " AND $table_name.date_created <= '{$query_args['adj_before']}'"; + $intervals_query['where_time_clause'] .= " AND $table_name.date_created >= '{$query_args['adj_after']}'"; $intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page']; } else { if ( 'asc' === $query_args['order'] ) { @@ -520,7 +521,7 @@ class WC_Admin_Reports_Data_Store { if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) { $interval = $query_args['interval']; - $intervals_query['select_clause'] = WC_Admin_Reports_Interval::db_datetime_format( $interval ); + $intervals_query['select_clause'] = WC_Admin_Reports_Interval::db_datetime_format( $interval, $table_name ); } $intervals_query = array_merge( $intervals_query, $this->get_limit_sql_params( $query_args ) ); From a9a0ccd185ee7356aa0ce4c3af131115a411b6d3 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 2 Jan 2019 11:33:05 +0100 Subject: [PATCH 04/91] Added refund_amount to products lookup as it needs to be reported on product level. --- plugins/woocommerce-admin/includes/class-wc-admin-api-init.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 2a33aece4e4..cc7b0f6b8b9 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -401,6 +401,7 @@ class WC_Admin_Api_Init { 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), From 4eaa5d911f9f74d476d4b8a992a631ee11a47ed6 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 2 Jan 2019 13:35:40 +0100 Subject: [PATCH 05/91] WIP on segmenting for orders/stats. --- ...ass-wc-admin-reports-orders-data-store.php | 301 +++++++++++++++++- 1 file changed, 299 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index 186865a2f28..0bb64666677 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -192,10 +192,299 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } } + /** + * Returns SELECT clause statements to be used for segmenting query. + * + * @param array $query_args Query arguments supplied by the user. + * @param string $segmenting_table SQL table containing the segmenting info. + * + * @return string SELECT clause statements. + */ + protected function segment_selections( $query_args, $segmenting_table ) { + global $wpdb; + $order_stats_table = $wpdb->prefix . 'wc_order_stats'; + // TODO: confirm with TeamX where to use numbers for full orders and where to use partial numbers for products within orders. + $columns_mapping = array( + 'orders_count' => "COUNT(DISTINCT($segmenting_table.order_id)) as orders_count", + 'num_items_sold' => "SUM($segmenting_table.product_qty) as num_items_sold", + 'gross_revenue' => "SUM($segmenting_table.product_gross_revenue) AS gross_revenue", + 'coupons' => "SUM($segmenting_table.coupon_amount) AS coupons", + 'refunds' => "SUM($segmenting_table.refund_amount) AS refunds", + 'taxes' => "SUM($segmenting_table.tax_amount) AS taxes", + 'shipping' => "SUM($segmenting_table.shipping_amount) AS shipping", + 'net_revenue' => "SUM($order_stats_table.net_total) AS net_revenue", // $segmenting_table.product_net_revenue. + 'avg_items_per_order' => "SUM($order_stats_table.num_items_sold) / COUNT(DISTINCT($order_stats_table.order_id)) AS avg_items_per_order", + 'avg_order_value' => "SUM($order_stats_table.net_total) / COUNT(DISTINCT($order_stats_table.order_id)) AS avg_order_value", + 'num_returning_customers' => "SUM($order_stats_table.returning_customer = 1) AS num_returning_customers", + 'num_new_customers' => "SUM($order_stats_table.returning_customer = 0) AS num_new_customers", + ); + + if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { + $keep = array(); + foreach ( $query_args['fields'] as $field ) { + if ( isset( $columns_mapping[ $field ] ) ) { + $keep[ $field ] = $columns_mapping[ $field ]; + } + } + $selections = implode( ', ', $keep ); + } else { + $selections = implode( ', ', $columns_mapping ); + } + 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' => $this->cast_numbers( $segment_data ), + ); + $segment_result[] = $segment_datum; + } + + return $segment_result; + } + + /** + * 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' => $this->cast_numbers( $segment_data ), + ); + $aggregated_segment_result[ $time_interval ]['segments'][] = $segment_datum; + } + + return $aggregated_segment_result; + } + + /** + * Retrieves and returns segments for 'totals' section of the result. + * + * @param array $query_args Query arguments provided by the user. + * @param array $totals_query Array containing the SQL clauses for 'totals' section of the result. + * @param string $table_name SQL table name used by the data store. + * + * @return array|void Array of segments for totals section of the result. + */ + protected function get_totals_segments( $query_args, $totals_query, $table_name ) { + global $wpdb; + if ( ! isset( $query_args['segmentby'] ) ) { + return; + } + + $segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + $segmenting_from = ''; + $segmenting_where = ''; + $segmenting_groupby = ''; + if ( 'product' === $query_args['segmentby'] ) { + $segmenting_groupby = $segmenting_table . '.product_id'; + } elseif ( 'variation' === $query_args['segmentby'] ) { + $segmenting_groupby = $segmenting_table . '.variation_id'; + } elseif ( 'category' === $query_args['segmentby'] ) { + // If we wanted category name as well, then we need to add the following: + // LEFT JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id, then wp_terms.name is category name + // RIGHT JOIN so that all categories are added. + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}term_relationships ON {$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'; + } elseif ( 'tax' === $query_args['segmentby'] ) { + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) + "; + $segmenting_where = " AND (order_items.order_item_type = 'tax')"; + $segmenting_groupby = 'order_items.order_item_name'; + } elseif ( 'coupon' === $query_args['segmentby'] ) { + $segmenting_from = " + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) + "; + $segmenting_where = " AND order_items.order_item_type = 'coupon'"; + $segmenting_groupby = 'order_items.order_item_name'; + } elseif ( 'customer_type' === $query_args['segmentby'] ) { + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}wc_order_stats AS customer_type_tbl ON ($segmenting_table.order_id = customer_type_tbl.order_id) + "; + $segmenting_groupby = 'customer_type_tbl.returning_customer'; + } + + $segment_selections = $this->segment_selections( $query_args, $segmenting_table ); + $totals_segments = $wpdb->get_results( + "SELECT + $segment_selections, + $segmenting_groupby + FROM + $table_name + INNER JOIN $segmenting_table ON ($table_name.order_id = $segmenting_table.order_id) + $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 ); + + // TODO: Fill in zeroes for second dimension. + return $totals_segments; + } + + /** + * 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; + } + 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']; + } + } + + /** + * Retrieves and returns segments for 'intervals' section of the result. + * + * @param array $query_args Query arguments provided by the user. + * @param array $intervals_query Array containing the SQL clauses for 'intervals' section of the result. + * @param string $table_name SQL table name used by the data store. + * + * @return array|void Array of segments for intervals section of the result. + */ + protected function get_intervals_segments( $query_args, $intervals_query, $table_name ) { + global $wpdb; + if ( ! isset( $query_args['segmentby'] ) ) { + return; + } + + $segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; + $segmenting_from = ''; + $segmenting_where = ''; + $segmenting_groupby = ''; + if ( 'product' === $query_args['segmentby'] ) { + $segmenting_groupby = $segmenting_table . '.product_id'; + } elseif ( 'variation' === $query_args['segmentby'] ) { + $segmenting_groupby = $segmenting_table . '.variation_id'; + } elseif ( 'category' === $query_args['segmentby'] ) { + // If we wanted category name as well, then we need to add the following: + // LEFT JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id, then wp_terms.name is category name + // RIGHT JOIN so that all categories are added. + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}term_relationships ON {$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'; + } elseif ( 'tax' === $query_args['segmentby'] ) { + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) + "; + $segmenting_where = " AND (order_items.order_item_type = 'tax')"; + $segmenting_groupby = 'order_items.order_item_name'; + } elseif ( 'coupon' === $query_args['segmentby'] ) { + $segmenting_from = " + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) + "; + $segmenting_where = " AND order_items.order_item_type = 'coupon'"; + $segmenting_groupby = 'order_items.order_item_name'; + } elseif ( 'customer_type' === $query_args['segmentby'] ) { + $segmenting_from = " + LEFT JOIN {$wpdb->prefix}wc_order_stats AS customer_type_tbl ON ($segmenting_table.order_id = customer_type_tbl.order_id) + "; + $segmenting_groupby = 'customer_type_tbl.returning_customer'; + } + + // TODO: finish limit update. + $segmenting_limit = ''; + + $segment_selections = $this->segment_selections( $query_args, $segmenting_table ); + $intervals_segments = $wpdb->get_results( + "SELECT + MAX($table_name.date_created) AS datetime_anchor, + {$intervals_query['select_clause']} AS time_interval, + $segment_selections, + $segmenting_groupby + FROM + $table_name + INNER JOIN $segmenting_table ON ($table_name.order_id = $segmenting_table.order_id) + $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 ); + + // TODO: Fill in zeroes for second dimension. + return $intervals_segments; + } + /** * 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. */ @@ -216,6 +505,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ), 'interval' => 'week', 'fields' => '*', + 'segmentby' => '', 'match' => 'all', 'status_is' => array(), @@ -267,6 +557,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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[0]['segments'] = $this->get_totals_segments( $query_args, $totals_query, $totals, $table_name ); + $totals = (object) $this->cast_numbers( $totals[0] ); $db_intervals = $wpdb->get_col( @@ -291,7 +583,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp return $data; } - $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count ); + $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name ); if ( '' !== $selections ) { $selections = ', ' . $selections; @@ -321,6 +613,11 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); } + $intervals_segments = $this->get_intervals_segments( $query_args, $intervals_query, $intervals, $table_name ); + + // Pigeon hole segments. + $this->assign_segments_to_intervals( $intervals, $intervals_segments ); + $data = (object) array( 'totals' => $totals, 'intervals' => $intervals, From 4cc042ebd8a7946647e75ea6500de6e901a69b34 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 2 Jan 2019 15:26:33 +0100 Subject: [PATCH 06/91] Added back updates to product lookup update routine after wc-admin-order-functions.php was removed and code moved to individual data stores. --- ...s-wc-admin-reports-products-data-store.php | 87 ++++++++++++++++--- 1 file changed, 76 insertions(+), 11 deletions(-) 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 df0a74211b3..c5c3876a755 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 @@ -320,7 +320,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i $wpdb->prefix . self::TABLE_NAME, array( 'order_id' => $order->get_id() ), array( '%d' ) - ); + ); // WPCS: cache ok, DB call ok. return; } @@ -330,24 +330,89 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i $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; + + // Shipping amount based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php + // distributed simply based on number of line items. + $order_items = $order->get_item_count(); + $refunded = $order->get_total_shipping_refunded(); + if ( $refunded > 0 ) { + $total_shipping_amount = $order->get_shipping_total() - $refunded; + } else { + $total_shipping_amount = $order->get_shipping_total(); + } + $shipping_amount = $total_shipping_amount / $order_items; + + // Shipping amount tax based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php + // distribute simply based on number of line items. + $shipping_tax_amount = 0; + // TODO: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? + if ( wc_tax_enabled() ) { + $order_taxes = $order->get_taxes(); + $line_items_shipping = $order->get_items( 'shipping' ); + $total_shipping_tax_amount = 0; + foreach ( $line_items_shipping as $item_id => $item ) { + $tax_data = $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 = $order->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; + } + } + } + } + $shipping_tax_amount = $total_shipping_tax_amount / $order_items; + } + + // Tax amount. + // TODO: check if this calculates tax correctly with refunds. + $tax_amount = 0; + if ( wc_tax_enabled() ) { + $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; + + // Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php. + $coupon_amount = $order_item->get_subtotal( 'edit' ) - $order_item->get_total( 'edit' ); + 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' => $order_item->get_quantity( 'edit' ) - $quantity_refunded, + 'product_net_revenue' => $net_revenue, + 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), + // TODO: is it needed? can we even recover the info? + 'price' => $order_item->get_subtotal( 'edit' ) / $order_item->get_quantity( 'edit' ), + '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', @@ -359,7 +424,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i '%f', '%s', ) - ); + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. } } } From 52e1ed920f9b8e0cf6c6ee839225e01c86ea813c Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 2 Jan 2019 15:29:43 +0100 Subject: [PATCH 07/91] Added placeholders for new db columns. --- .../class-wc-admin-reports-products-data-store.php | 7 +++++++ 1 file changed, 7 insertions(+) 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 c5c3876a755..aaa55dd5aba 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 @@ -423,6 +423,13 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i '%d', '%f', '%s', + '%f', + '%f', + '%f', + '%f', + '%f', + '%f', + '%f', ) ); // WPCS: cache ok, DB call ok, unprepared SQL ok. } From 6bceabe0956f98d2c58242a86bd5a23a3355ad0b Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 2 Jan 2019 15:40:47 +0100 Subject: [PATCH 08/91] Fixed extra parameters after refactoring. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index 0e0e3ae0add..dbd19b1bb5a 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -558,7 +558,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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[0]['segments'] = $this->get_totals_segments( $query_args, $totals_query, $totals, $table_name ); + $totals[0]['segments'] = $this->get_totals_segments( $query_args, $totals_query, $table_name ); $totals = (object) $this->cast_numbers( $totals[0] ); @@ -614,7 +614,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); } - $intervals_segments = $this->get_intervals_segments( $query_args, $intervals_query, $intervals, $table_name ); + $intervals_segments = $this->get_intervals_segments( $query_args, $intervals_query, $table_name ); // Pigeon hole segments. $this->assign_segments_to_intervals( $intervals, $intervals_segments ); From 13022fee4f7fc67d6cc0aa9d47730d6310ce1e74 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Thu, 3 Jan 2019 18:59:07 +0100 Subject: [PATCH 09/91] WIP on orders/stats segmenting. --- ...ass-wc-admin-reports-orders-data-store.php | 140 ++++++++++++++++-- 1 file changed, 128 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index dbd19b1bb5a..bf6949f2035 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -256,7 +256,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'segment_id' => $segment_id, 'subtotals' => $this->cast_numbers( $segment_data ), ); - $segment_result[] = $segment_datum; + $segment_result[ $segment_id ] = $segment_datum; } return $segment_result; @@ -291,22 +291,137 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'segment_id' => $segment_id, 'subtotals' => $this->cast_numbers( $segment_data ), ); - $aggregated_segment_result[ $time_interval ]['segments'][] = $segment_datum; + $aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum; } return $aggregated_segment_result; } + /** + * Return all segments for given segmentby parameter. + * + * @param array $query_args Query args provided by the user. + * + * @return array|void + */ + protected function get_all_segments( $query_args ) { + global $wpdb; + if ( ! isset( $query_args['segmentby'] ) ) { + return; + } + + if ( 'product' === $query_args['segmentby'] ) { + $segments = wc_get_products( + array( + 'return' => 'ids', + 'limit' => -1, + ) + ); + } elseif ( 'variation' === $query_args['segmentby'] ) { + // TODO: assuming that this will only be used for one product, check assumption. + if ( ! isset( $query_args['products'] ) || count( $query_args['products'] ) !== 1 ) { + return; + } + + $segments = wc_get_products( + array( + 'return' => 'ids', + 'limit' => - 1, + 'type' => 'variation', + 'parent' => $query_args['products'][0], + ) + ); + } elseif ( 'category' === $query_args['segmentby'] ) { + $categories = get_categories( + array( + 'taxonomy' => 'product_cat', + ) + ); + $segments = wp_list_pluck( $categories, 'cat_ID' ); + } elseif ( 'tax' === $query_args['segmentby'] ) { + // There are two types of taxes: taxes that are currently in the db and taxes that were used in sold products before. + // Taxes from the db. + $current_taxes = $wpdb->get_results( "SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates", ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + $current_taxes = wp_list_pluck( $current_taxes, 'tax_rate_id' ); + // taxes from sold products. + $sold_taxes = $wpdb->get_results( "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. + $sold_taxes = wp_list_pluck( $sold_taxes, 'meta_value' ); + $segments = array_unique( array_merge( $current_taxes, $sold_taxes ) ); + } elseif ( 'coupon' === $query_args['segmentby'] ) { + $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' === $query_args['segmentby'] ) { + // 0 -- new customer + // 1 -- returning customer + $segments = array( 0, 1 ); + } + + return $segments; + } + + /** + * Adds zeroes for segments not present in the data selection. + * + * @param array $query_args Query arguments provided by the user. + * @param array $segments Array of segments from the database for given data points. + * @param array $all_segment_ids Array of all possible segment ids. + * + * @return array + */ + protected function fill_in_missing_segments( $query_args, $segments, $all_segment_ids ) { + + $segment_subtotals = array(); + if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { + foreach ( $query_args['fields'] as $field ) { + if ( isset( $this->report_columns[ $field ] ) ) { + $segment_subtotals[ $field ] = 0; + } + } + } else { + foreach ( $this->report_columns as $field ) { + $segment_subtotals[ $field ] = 0; + } + } + if ( ! is_array( $segments ) ) { + $segments = array(); + } + 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. + return array_values( $segments ); + } + + /** + * Adds missing segments to intervals, modifies $data. + * + * @param array $query_args Query arguments provided by the user. + * @param stdClass $data Response data. + * @param array $all_segment_ids Array of all possible segment ids. + */ + protected function fill_in_missing_interval_segments( $query_args, &$data, $all_segment_ids ) { + foreach ( $data->intervals as $order_id => $interval_data ) { + $data->intervals[ $order_id ]['segments'] = $this->fill_in_missing_segments( $query_args, $data->intervals[ $order_id ]['segments'], $all_segment_ids ); + } + } + /** * Retrieves and returns segments for 'totals' section of the result. * * @param array $query_args Query arguments provided by the user. * @param array $totals_query Array containing the SQL clauses for 'totals' section of the result. * @param string $table_name SQL table name used by the data store. + * @param array $all_segments Array of all possible segment ids. * * @return array|void Array of segments for totals section of the result. */ - protected function get_totals_segments( $query_args, $totals_query, $table_name ) { + protected function get_totals_segments( $query_args, $totals_query, $table_name, $all_segments ) { global $wpdb; if ( ! isset( $query_args['segmentby'] ) ) { return; @@ -338,10 +453,9 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $segmenting_groupby = 'order_items.order_item_name'; } elseif ( 'coupon' === $query_args['segmentby'] ) { $segmenting_from = " - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) + INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($segmenting_table.order_id = coupon_lookup.order_id) "; - $segmenting_where = " AND order_items.order_item_type = 'coupon'"; - $segmenting_groupby = 'order_items.order_item_name'; + $segmenting_groupby = 'coupon_lookup.coupon_id'; } elseif ( 'customer_type' === $query_args['segmentby'] ) { $segmenting_from = " LEFT JOIN {$wpdb->prefix}wc_order_stats AS customer_type_tbl ON ($segmenting_table.order_id = customer_type_tbl.order_id) @@ -371,7 +485,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp // Reformat result. $totals_segments = $this->reformat_totals_segments( $totals_segments, $segmenting_groupby ); - // TODO: Fill in zeroes for second dimension. + $totals_segments = $this->fill_in_missing_segments( $query_args, $totals_segments, $all_segments ); + return $totals_segments; } @@ -439,10 +554,9 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $segmenting_groupby = 'order_items.order_item_name'; } elseif ( 'coupon' === $query_args['segmentby'] ) { $segmenting_from = " - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) + INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($segmenting_table.order_id = coupon_lookup.order_id) "; - $segmenting_where = " AND order_items.order_item_type = 'coupon'"; - $segmenting_groupby = 'order_items.order_item_name'; + $segmenting_groupby = 'coupon_lookup.coupon_id'; } elseif ( 'customer_type' === $query_args['segmentby'] ) { $segmenting_from = " LEFT JOIN {$wpdb->prefix}wc_order_stats AS customer_type_tbl ON ($segmenting_table.order_id = customer_type_tbl.order_id) @@ -479,7 +593,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp // Reformat result. $intervals_segments = $this->reformat_intervals_segments( $intervals_segments, $segmenting_groupby ); - // TODO: Fill in zeroes for second dimension. + // Missing segments will be filled once all the intervals are added. return $intervals_segments; } @@ -558,7 +672,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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[0]['segments'] = $this->get_totals_segments( $query_args, $totals_query, $table_name ); + $all_segments = $this->get_all_segments( $query_args ); + $totals[0]['segments'] = $this->get_totals_segments( $query_args, $totals_query, $table_name, $all_segments ); $totals = (object) $this->cast_numbers( $totals[0] ); @@ -631,6 +746,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data ); $this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] ); $this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] ); + $this->fill_in_missing_interval_segments( $query_args, $data, $all_segments ); } else { $this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $data->intervals ); } From 411ff5978819e2e7f1f1b89a82812d65574ba44e Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 11:11:23 +0100 Subject: [PATCH 10/91] WIP on orders/stats segmenting. --- .../includes/class-wc-admin-api-init.php | 1 - ...ass-wc-admin-reports-orders-data-store.php | 750 ++++++++++++++---- ...s-wc-admin-reports-products-data-store.php | 47 +- 3 files changed, 599 insertions(+), 199 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 7e819d42d11..82ebad4ba7a 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -414,7 +414,6 @@ class WC_Admin_Api_Init { product_qty INT UNSIGNED NOT NULL, product_net_revenue double DEFAULT 0 NOT NULL, product_gross_revenue double DEFAULT 0 NOT NULL, - price 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, diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index bf6949f2035..92afec105c9 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -49,6 +49,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'num_returning_customers' => 'intval', 'num_new_customers' => 'intval', 'products' => 'intval', + 'segment_id' => 'intval', ); /** @@ -194,32 +195,56 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } /** - * Returns SELECT clause statements to be used for segmenting query. + * Returns SELECT clause statements to be used for product-related product-level segmenting query (e.g. products sold, revenue from product X when segmenting by category). * * @param array $query_args Query arguments supplied by the user. - * @param string $segmenting_table SQL table containing the segmenting info. + * @param string $products_table Name of SQL table containing the product-level segmenting info. * * @return string SELECT clause statements. */ - protected function segment_selections( $query_args, $segmenting_table ) { - global $wpdb; - $order_stats_table = $wpdb->prefix . 'wc_order_stats'; - // TODO: confirm with TeamX where to use numbers for full orders and where to use partial numbers for products within orders. + protected function get_segment_selections_product_level( $query_args, $products_table ) { $columns_mapping = array( - 'orders_count' => "COUNT(DISTINCT($segmenting_table.order_id)) as orders_count", - 'num_items_sold' => "SUM($segmenting_table.product_qty) as num_items_sold", - 'gross_revenue' => "SUM($segmenting_table.product_gross_revenue) AS gross_revenue", - 'coupons' => "SUM($segmenting_table.coupon_amount) AS coupons", - 'refunds' => "SUM($segmenting_table.refund_amount) AS refunds", - 'taxes' => "SUM($segmenting_table.tax_amount) AS taxes", - 'shipping' => "SUM($segmenting_table.shipping_amount) AS shipping", - 'net_revenue' => "SUM($order_stats_table.net_total) AS net_revenue", // $segmenting_table.product_net_revenue. - 'avg_items_per_order' => "SUM($order_stats_table.num_items_sold) / COUNT(DISTINCT($order_stats_table.order_id)) AS avg_items_per_order", - 'avg_order_value' => "SUM($order_stats_table.net_total) / COUNT(DISTINCT($order_stats_table.order_id)) AS avg_order_value", - 'num_returning_customers' => "SUM($order_stats_table.returning_customer = 1) AS num_returning_customers", - 'num_new_customers' => "SUM($order_stats_table.returning_customer = 0) AS num_new_customers", + 'num_items_sold' => "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", + 'net_revenue' => "SUM($products_table.product_net_revenue) - SUM($products_table.refund_amount) AS net_revenue", ); + return $this->prepare_selections( $query_args, $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 array $query_args Query arguments supplied by the user. + * @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( $query_args, $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( $query_args, $columns_mapping ); + } + + /** + * Filters definitions for SELECT clauses based on query_args and joins them into one string usable in SELECT clause. + * + * @param array $query_args Query arguments supplied by the user. + * @param array $columns_mapping Column name -> SQL statememt mapping. + * + * @return string to be used in SELECT clause statements. + */ + protected function prepare_selections( $query_args, $columns_mapping ) { if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { $keep = array(); foreach ( $query_args['fields'] as $field ) { @@ -231,9 +256,46 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } else { $selections = implode( ', ', $columns_mapping ); } + + if ( $selections ) { + $selections = ',' . $selections; + } + return $selections; } + /** + * 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 array $query_args Query arguments supplied by the user. + * @param string $table_name 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( $query_args, $table_name, $overrides = array() ) { + $columns_mapping = array( + 'num_items_sold' => "SUM($table_name.num_items_sold) as num_items_sold", + 'gross_revenue' => "SUM($table_name.gross_total) AS gross_revenue", + 'coupons' => "SUM($table_name.coupon_total) AS coupons", + 'refunds' => "SUM($table_name.refund_total) AS refunds", + 'taxes' => "SUM($table_name.tax_total) AS taxes", + 'shipping' => "SUM($table_name.shipping_total) AS shipping", + 'net_revenue' => "SUM($table_name.net_total) - SUM($table_name.refund_total) AS net_revenue", + 'orders_count' => "COUNT($table_name.order_id) AS orders_count", + 'avg_items_per_order' => "AVG($table_name.num_items_sold) AS avg_items_per_order", + 'avg_order_value' => "(SUM($table_name.net_total) - SUM($table_name.refund_total))/COUNT($table_name.order_id) AS avg_order_value", + 'num_returning_customers' => "SUM($table_name.returning_customer) AS num_returning_customers", + 'num_new_customers' => "COUNT($table_name.returning_customer) - SUM($table_name.returning_customer) AS num_new_customers", + ); + + if ( $overrides ) { + $columns_mapping = array_merge( $columns_mapping, $overrides ); + } + + return $this->prepare_selections( $query_args, $columns_mapping ); + } + /** * Update row-level db result for segments in 'totals' section to the format used for output. * @@ -252,7 +314,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp foreach ( $segments_db_result as $segment_data ) { $segment_id = $segment_data[ $segment_dimension ]; unset( $segment_data[ $segment_dimension ] ); - $segment_datum = array( + $segment_datum = array( 'segment_id' => $segment_id, 'subtotals' => $this->cast_numbers( $segment_data ), ); @@ -262,6 +324,143 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 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' => $this->cast_numbers( $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. * @@ -319,7 +518,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp ); } elseif ( 'variation' === $query_args['segmentby'] ) { // TODO: assuming that this will only be used for one product, check assumption. - if ( ! isset( $query_args['products'] ) || count( $query_args['products'] ) !== 1 ) { + if ( ! isset( $query_args['product_includes'] ) || count( $query_args['product_includes'] ) !== 1 ) { return; } @@ -328,7 +527,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'return' => 'ids', 'limit' => - 1, 'type' => 'variation', - 'parent' => $query_args['products'][0], + 'parent' => $query_args['product_includes'][0], ) ); } elseif ( 'category' === $query_args['segmentby'] ) { @@ -338,15 +537,6 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp ) ); $segments = wp_list_pluck( $categories, 'cat_ID' ); - } elseif ( 'tax' === $query_args['segmentby'] ) { - // There are two types of taxes: taxes that are currently in the db and taxes that were used in sold products before. - // Taxes from the db. - $current_taxes = $wpdb->get_results( "SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates", ARRAY_A ); // WPCS: cache ok, DB call ok, unprepared SQL ok. - $current_taxes = wp_list_pluck( $current_taxes, 'tax_rate_id' ); - // taxes from sold products. - $sold_taxes = $wpdb->get_results( "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. - $sold_taxes = wp_list_pluck( $sold_taxes, 'meta_value' ); - $segments = array_unique( array_merge( $current_taxes, $sold_taxes ) ); } elseif ( 'coupon' === $query_args['segmentby'] ) { $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' ); @@ -412,65 +602,78 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } /** - * Retrieves and returns segments for 'totals' section of the result. + * Calculate segments for segmenting property bound to product (e.g. category, product_id, variation_id). * - * @param array $query_args Query arguments provided by the user. - * @param array $totals_query Array containing the SQL clauses for 'totals' section of the result. - * @param string $table_name SQL table name used by the data store. - * @param array $all_segments Array of all possible segment ids. + * @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. + * @param array $all_segment_ids Array of all possible segment ids. * - * @return array|void Array of segments for totals section of the result. + * @return array */ - protected function get_totals_segments( $query_args, $totals_query, $table_name, $all_segments ) { + 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, $all_segment_ids ) { + 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, $all_segment_ids ); + } + } + + /** + * 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 array $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. + * @param array $all_segment_ids Array of all possible segment ids. + * + * @return array + */ + protected function get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params, $all_segment_ids ) { + 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, $all_segment_ids ); + } + } + + /** + * 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; - if ( ! isset( $query_args['segmentby'] ) ) { - return; - } - $segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; - $segmenting_from = ''; - $segmenting_where = ''; - $segmenting_groupby = ''; - if ( 'product' === $query_args['segmentby'] ) { - $segmenting_groupby = $segmenting_table . '.product_id'; - } elseif ( 'variation' === $query_args['segmentby'] ) { - $segmenting_groupby = $segmenting_table . '.variation_id'; - } elseif ( 'category' === $query_args['segmentby'] ) { - // If we wanted category name as well, then we need to add the following: - // LEFT JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id, then wp_terms.name is category name - // RIGHT JOIN so that all categories are added. - $segmenting_from = " - LEFT JOIN {$wpdb->prefix}term_relationships ON {$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'; - } elseif ( 'tax' === $query_args['segmentby'] ) { - $segmenting_from = " - LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) - "; - $segmenting_where = " AND (order_items.order_item_type = 'tax')"; - $segmenting_groupby = 'order_items.order_item_name'; - } elseif ( 'coupon' === $query_args['segmentby'] ) { - $segmenting_from = " - INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($segmenting_table.order_id = coupon_lookup.order_id) - "; - $segmenting_groupby = 'coupon_lookup.coupon_id'; - } elseif ( 'customer_type' === $query_args['segmentby'] ) { - $segmenting_from = " - LEFT JOIN {$wpdb->prefix}wc_order_stats AS customer_type_tbl ON ($segmenting_table.order_id = customer_type_tbl.order_id) - "; - $segmenting_groupby = 'customer_type_tbl.returning_customer'; - } + $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; - $segment_selections = $this->segment_selections( $query_args, $segmenting_table ); - $totals_segments = $wpdb->get_results( + // 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 - $segment_selections, - $segmenting_groupby + $segmenting_groupby AS $segmenting_dimension_name + {$segmenting_selections['product_level']} FROM $table_name - INNER JOIN $segmenting_table ON ($table_name.order_id = $segmenting_table.order_id) $segmenting_from {$totals_query['from_clause']} WHERE @@ -482,14 +685,301 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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( 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. + * @param array $all_segment_ids Array of all possible segment ids. + * + * @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, $all_segment_ids ) { + 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( $all_segment_ids ); + + // 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( 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 ); - - $totals_segments = $this->fill_in_missing_segments( $query_args, $totals_segments, $all_segments ); - 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. + * @param array $all_segment_ids Ids of all possible segments. + * + * @return array + */ + protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query, $all_segment_ids ) { + global $wpdb; + $limit_parts = explode( ',', $intervals_query['limit'] ); + $orig_rowcount = intval( $limit_parts[1] ); + $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $all_segment_ids ); + + $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_args Parameters provided by the user. + * @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). + * @param array $all_segments Array of all segment ids. + * + * @return array|void + * @throws WC_REST_Exception In case of segmenting by variations, when no parent product is specified. + */ + protected function get_segments( $type, $query_args, $query_params, $table_name, $all_segments ) { + global $wpdb; + if ( ! isset( $query_args['segmentby'] ) ) { + return; + } + + $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' === $query_args['segmentby'] ) { + // TODO: how to handle shipping taxes when grouped by product? + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $query_args, $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $query_args, $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, $all_segments ); + } elseif ( 'variation' === $query_args['segmentby'] ) { + if ( ! isset( $query_args['product_includes'] ) || count( $query_args['product_includes'] ) !== 1 ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + } + + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $query_args, $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $query_args, $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 = {$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, $all_segments ); + } elseif ( 'category' === $query_args['segmentby'] ) { + $segmenting_selections = array( + 'product_level' => $this->get_segment_selections_product_level( $query_args, $product_segmenting_table ), + 'order_level' => $this->get_segment_selections_order_level( $query_args, $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, $all_segments ); + } elseif ( 'coupon' === $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( $query_args, $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, $all_segments ); + } elseif ( 'customer_type' === $query_args['segmentby'] ) { + $segmenting_selections = $this->segment_selections_orders( $query_args, $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, $all_segments ); + } + + return $segments; + } + /** * Assign segments to time intervals by updating original $intervals array. * @@ -513,90 +1003,6 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } } - /** - * Retrieves and returns segments for 'intervals' section of the result. - * - * @param array $query_args Query arguments provided by the user. - * @param array $intervals_query Array containing the SQL clauses for 'intervals' section of the result. - * @param string $table_name SQL table name used by the data store. - * - * @return array|void Array of segments for intervals section of the result. - */ - protected function get_intervals_segments( $query_args, $intervals_query, $table_name ) { - global $wpdb; - if ( ! isset( $query_args['segmentby'] ) ) { - return; - } - - $segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; - $segmenting_from = ''; - $segmenting_where = ''; - $segmenting_groupby = ''; - if ( 'product' === $query_args['segmentby'] ) { - $segmenting_groupby = $segmenting_table . '.product_id'; - } elseif ( 'variation' === $query_args['segmentby'] ) { - $segmenting_groupby = $segmenting_table . '.variation_id'; - } elseif ( 'category' === $query_args['segmentby'] ) { - // If we wanted category name as well, then we need to add the following: - // LEFT JOIN wp_terms ON wp_term_taxonomy.term_id = wp_terms.term_id, then wp_terms.name is category name - // RIGHT JOIN so that all categories are added. - $segmenting_from = " - LEFT JOIN {$wpdb->prefix}term_relationships ON {$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'; - } elseif ( 'tax' === $query_args['segmentby'] ) { - $segmenting_from = " - LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ($segmenting_table.order_id = order_items.order_id) - "; - $segmenting_where = " AND (order_items.order_item_type = 'tax')"; - $segmenting_groupby = 'order_items.order_item_name'; - } elseif ( 'coupon' === $query_args['segmentby'] ) { - $segmenting_from = " - INNER JOIN {$wpdb->prefix}wc_order_coupon_lookup AS coupon_lookup ON ($segmenting_table.order_id = coupon_lookup.order_id) - "; - $segmenting_groupby = 'coupon_lookup.coupon_id'; - } elseif ( 'customer_type' === $query_args['segmentby'] ) { - $segmenting_from = " - LEFT JOIN {$wpdb->prefix}wc_order_stats AS customer_type_tbl ON ($segmenting_table.order_id = customer_type_tbl.order_id) - "; - $segmenting_groupby = 'customer_type_tbl.returning_customer'; - } - - // TODO: finish limit update. - $segmenting_limit = ''; - - $segment_selections = $this->segment_selections( $query_args, $segmenting_table ); - $intervals_segments = $wpdb->get_results( - "SELECT - MAX($table_name.date_created) AS datetime_anchor, - {$intervals_query['select_clause']} AS time_interval, - $segment_selections, - $segmenting_groupby - FROM - $table_name - INNER JOIN $segmenting_table ON ($table_name.order_id = $segmenting_table.order_id) - $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 ); - - // Missing segments will be filled once all the intervals are added. - return $intervals_segments; - } - /** * Returns the report data based on parameters supplied by the user. * @@ -672,10 +1078,10 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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; - $all_segments = $this->get_all_segments( $query_args ); - $totals[0]['segments'] = $this->get_totals_segments( $query_args, $totals_query, $table_name, $all_segments ); - - $totals = (object) $this->cast_numbers( $totals[0] ); + $all_segments = $this->get_all_segments( $query_args ); + $segments = $this->get_segments( 'totals', $query_args, $totals_query, $table_name, $all_segments ); + $totals[0]['segments'] = $this->fill_in_missing_segments( $query_args, $segments, $all_segments ); + $totals = (object) $this->cast_numbers( $totals[0] ); $db_intervals = $wpdb->get_col( "SELECT @@ -729,7 +1135,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); } - $intervals_segments = $this->get_intervals_segments( $query_args, $intervals_query, $table_name ); + $intervals_segments = $this->get_segments( 'intervals', $query_args, $intervals_query, $table_name, $all_segments ); // Pigeon hole segments. $this->assign_segments_to_intervals( $intervals, $intervals_segments ); 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 aaa55dd5aba..09feb580f70 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 @@ -346,38 +346,35 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i // distribute simply based on number of line items. $shipping_tax_amount = 0; // TODO: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? - if ( wc_tax_enabled() ) { - $order_taxes = $order->get_taxes(); - $line_items_shipping = $order->get_items( 'shipping' ); - $total_shipping_tax_amount = 0; - foreach ( $line_items_shipping as $item_id => $item ) { - $tax_data = $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 = $order->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; - } + $order_taxes = $order->get_taxes(); + $line_items_shipping = $order->get_items( 'shipping' ); + $total_shipping_tax_amount = 0; + foreach ( $line_items_shipping as $item_id => $item ) { + $tax_data = $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 = $order->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; } } } - $shipping_tax_amount = $total_shipping_tax_amount / $order_items; } + $shipping_tax_amount = $total_shipping_tax_amount / $order_items; // Tax amount. // TODO: check if this calculates tax correctly with refunds. $tax_amount = 0; - if ( wc_tax_enabled() ) { - $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; - } + + $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? @@ -404,8 +401,6 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i 'product_qty' => $order_item->get_quantity( 'edit' ) - $quantity_refunded, 'product_net_revenue' => $net_revenue, 'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), - // TODO: is it needed? can we even recover the info? - 'price' => $order_item->get_subtotal( 'edit' ) / $order_item->get_quantity( 'edit' ), 'coupon_amount' => $coupon_amount, 'tax_amount' => $tax_amount, 'shipping_amount' => $shipping_amount, From 69f63f48d22ee5a4fc71ed774eb595cd4fc1a63e Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 11:25:49 +0100 Subject: [PATCH 11/91] Ain't Nobody Got Time for That. --- .../api/class-wc-admin-rest-reports-orders-stats-controller.php | 2 -- 1 file changed, 2 deletions(-) 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 5e6617e4aa0..be8c030bfb4 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 @@ -445,9 +445,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont 'product', 'category', 'variation', - 'tax', 'coupon', - 'billing_country', // just to test out if it's possible. 'customer_type', // new vs returning. ), 'validate_callback' => 'rest_validate_request_arg', From d14426cbc1075243b20680543c93d57a7c14e15e Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 11:30:43 +0100 Subject: [PATCH 12/91] Corrected type of parameter in the docblock. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index b467d8c4266..e6fb75f5999 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -627,7 +627,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp * 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 array $segmenting_select SELECT part of segmenting SQL query. + * @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. From ff6b4ffb26b785d048c0461c2683e048ae6fd7d5 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 11:31:21 +0100 Subject: [PATCH 13/91] Added refund_total to unique orders table to be able to calculate net_total correctly. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index e6fb75f5999..bd23242af14 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -697,6 +697,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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 From 5edc60d30b0c4517eefa225bc7fddc91ca695372 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 11:32:59 +0100 Subject: [PATCH 14/91] Added refund_total also to intervals (to be able to calculate net_total correctly). --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index bd23242af14..e955f9e69f3 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -784,6 +784,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp $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 From 7308aa415b21fa28c5393c19c310bc1dad851eed Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 15:22:19 +0100 Subject: [PATCH 15/91] Schema updates. --- ...s-wc-admin-rest-reports-orders-stats-controller.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 be8c030bfb4..427175cd754 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 @@ -166,6 +166,12 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont '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', @@ -225,6 +231,8 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont // 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', @@ -281,7 +289,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Cont 'type' => 'object', 'context' => array( 'view', 'edit' ), 'readonly' => true, - 'properties' => $data_values, + 'properties' => $intervals, ), ), ), From ebe76c2d430c5b2c1f122d26239620e6911dac78 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 15:23:13 +0100 Subject: [PATCH 16/91] Added missing parameter to new data stores. --- .../class-wc-admin-reports-downloads-stats-data-store.php | 2 +- .../class-wc-admin-reports-products-stats-data-store.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-stats-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-stats-data-store.php index c08e6bc9256..69df11ac3b7 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-stats-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-downloads-stats-data-store.php @@ -107,7 +107,7 @@ class WC_Admin_Reports_Downloads_Stats_Data_Store extends WC_Admin_Reports_Downl return array(); } - $this->update_intervals_sql_params( $intervals_query, $query_args, $db_records_count, $expected_interval_count ); + $this->update_intervals_sql_params( $intervals_query, $query_args, $db_records_count, $expected_interval_count, $table_name ); $intervals_query['where_time_clause'] = str_replace( 'date_created', 'timestamp', $intervals_query['where_time_clause'] ); $totals = $wpdb->get_results( 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 44ec6bcb364..9e404abc797 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 @@ -136,7 +136,7 @@ class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Produc return array(); } - $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count ); + $this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count, $table_name ); $totals = $wpdb->get_results( "SELECT From 26925872ec1e2a15e3919a98291b979730c07b03 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 15:25:17 +0100 Subject: [PATCH 17/91] Corrected return types so that no segmented queries work fine. --- ...class-wc-admin-reports-orders-data-store.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index e955f9e69f3..96d65bb471c 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -499,12 +499,12 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp * * @param array $query_args Query args provided by the user. * - * @return array|void + * @return array */ protected function get_all_segments( $query_args ) { global $wpdb; - if ( ! isset( $query_args['segmentby'] ) ) { - return; + if ( ! isset( $query_args['segmentby'] ) || '' === $query_args['segmentby'] ) { + return array(); } if ( 'product' === $query_args['segmentby'] ) { @@ -517,7 +517,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } elseif ( 'variation' === $query_args['segmentby'] ) { // TODO: assuming that this will only be used for one product, check assumption. if ( ! isset( $query_args['product_includes'] ) || count( $query_args['product_includes'] ) !== 1 ) { - return; + return array(); } $segments = wc_get_products( @@ -900,13 +900,13 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). * @param array $all_segments Array of all segment ids. * - * @return array|void + * @return array * @throws WC_REST_Exception In case of segmenting by variations, when no parent product is specified. */ protected function get_segments( $type, $query_args, $query_params, $table_name, $all_segments ) { global $wpdb; - if ( ! isset( $query_args['segmentby'] ) ) { - return; + if ( ! isset( $query_args['segmentby'] ) || '' === $query_args['segmentby'] ) { + return array(); } $product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup'; @@ -989,7 +989,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 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'] ] = $interval; + $intervals[ $interval['time_interval'] ]['segments'] = array(); } foreach ( $old_keys as $key ) { unset( $intervals[ $key ] ); From bc68976fb7329072b158535f1714a3ea37dac77d Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 15:27:24 +0100 Subject: [PATCH 18/91] Updated schema tests to reflect latest changes. --- .../tests/api/reports-orders-stats.php | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/tests/api/reports-orders-stats.php b/plugins/woocommerce-admin/tests/api/reports-orders-stats.php index 61184061a40..a4b24bacfd1 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 */ class WC_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case { @@ -86,11 +89,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 ) ); @@ -102,10 +111,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->assertEquals( 9, 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->assertArrayHasKey( 'num_items_sold', $totals ); + $this->assertArrayHasKey( 'coupons', $totals ); + $this->assertArrayHasKey( 'num_returning_customers', $totals ); + $this->assertArrayHasKey( 'num_new_customers', $totals ); + $this->assertArrayHasKey( 'segments', $totals ); } } From ab4eb674e2a2e0a166db084533a510baa4a1669d Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 15:28:28 +0100 Subject: [PATCH 19/91] Added segments fields to expected response in tests. --- .../reports/class-wc-tests-reports-orders.php | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php index 4709fd6284b..541096d7d70 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php @@ -66,6 +66,7 @@ class WC_Tests_Reports_Orders 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 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 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 extends WC_Unit_Test_Case { 'coupons' => 20, 'num_returning_customers' => 0, 'num_new_customers' => 1, + 'segments' => array(), ), ), ), @@ -374,6 +378,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -395,6 +400,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -461,6 +467,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -482,6 +489,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -534,6 +542,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -555,6 +564,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -590,6 +600,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 0, 'products' => 0, + 'segments' => array(), ), 'intervals' => array( array( @@ -611,6 +622,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => 0, 'num_returning_customers' => 0, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -667,6 +679,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -688,6 +701,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -740,6 +754,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 3, + 'segments' => array(), ), 'intervals' => array( array( @@ -761,6 +776,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -809,6 +825,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 2, + 'segments' => array(), // product 3 and product 4 (that is sometimes included in the orders with product 3). ), 'intervals' => array( @@ -831,6 +848,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -881,6 +899,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, // 'new' customer orders product 1 (their first order) 'products' => 3, + 'segments' => array(), ), 'intervals' => array( array( @@ -902,6 +921,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -951,6 +971,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, // 'new' customer orders product 1 (their first order) 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -972,6 +993,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1024,6 +1046,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, // 'new' customer orders product 1 (their first order) 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1045,6 +1068,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1099,6 +1123,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, // new customers' orders created without coupon. 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1120,6 +1145,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1171,6 +1197,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1192,6 +1219,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1243,6 +1271,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1264,6 +1293,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1317,6 +1347,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1338,6 +1369,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1394,6 +1426,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1415,6 +1448,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1455,6 +1489,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 2, 'products' => 1, + 'segments' => array(), ), 'intervals' => array( array( @@ -1476,6 +1511,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => 0, 'num_new_customers' => 2, + 'segments' => array(), ), ), ), @@ -1528,6 +1564,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1549,6 +1586,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1601,6 +1639,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1622,6 +1661,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -1677,6 +1717,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -1698,6 +1739,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1749,6 +1791,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1770,6 +1813,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1824,6 +1868,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1845,6 +1890,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1903,6 +1949,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -1924,6 +1971,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -1983,6 +2031,7 @@ class WC_Tests_Reports_Orders 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( @@ -2004,6 +2053,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2065,6 +2115,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -2086,6 +2137,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -2151,6 +2203,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, 'products' => 2, + 'segments' => array(), ), 'intervals' => array( array( @@ -2172,6 +2225,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count, 'num_new_customers' => 0, + 'segments' => array(), ), ), ), @@ -2229,6 +2283,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2250,6 +2305,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2306,6 +2362,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2327,6 +2384,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2383,6 +2441,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2404,6 +2463,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2460,6 +2520,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2481,6 +2542,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2537,6 +2599,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2558,6 +2621,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2617,6 +2681,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2638,6 +2703,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2700,6 +2766,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2721,6 +2788,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2783,6 +2851,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2804,6 +2873,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2869,6 +2939,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2890,6 +2961,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), @@ -2958,6 +3030,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, 'products' => 4, + 'segments' => array(), ), 'intervals' => array( array( @@ -2979,6 +3052,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { 'avg_order_value' => $net_revenue / $orders_count, 'num_returning_customers' => $orders_count - $new_customers, 'num_new_customers' => $new_customers, + 'segments' => array(), ), ), ), From 805925165674afe2b85f47fff233a709c250553e Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 15:29:03 +0100 Subject: [PATCH 20/91] Updated type for products field in response. --- .../tests/reports/class-wc-tests-reports-revenue-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-revenue-stats.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-revenue-stats.php index f417d1bda92..d74bff5c04b 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,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, - 'products' => '1', + 'products' => 1, ), 'intervals' => array( array( From 562e21ff540092dac52dd1659ab7c4810743c118 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 18:20:43 +0100 Subject: [PATCH 21/91] Replace time interval keys with numeric ones to format REST response correctly. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index 96d65bb471c..4707410a704 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -1002,6 +1002,9 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } $intervals[ $time_interval ]['segments'] = $segment['segments']; } + + // To remove time interval keys (so that REST response is formatted correctly). + $intervals = array_values( $intervals ); } /** From 20090a90e45141e3246406ae9948e85c35e64991 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 11 Jan 2019 18:39:57 +0100 Subject: [PATCH 22/91] Added segments property to revenue test. --- .../tests/reports/class-wc-tests-reports-revenue-stats.php | 4 ++++ 1 file changed, 4 insertions(+) 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 d74bff5c04b..b09cc74afa6 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 @@ -63,6 +63,7 @@ class WC_Admin_Tests_Reports_Revenue_Stats extends WC_Unit_Test_Case { 'num_returning_customers' => 0, 'num_new_customers' => 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(), ), ), ), From 13ec8e2851d250f2fd9904ac0dbdb510db0a47c4 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Mon, 14 Jan 2019 16:10:13 +0100 Subject: [PATCH 23/91] FOrmula for shipping distribution among line items updated to reflect qty, so that sum of shipping per line item equals total shipping amount. --- .../class-wc-admin-reports-products-data-store.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 6d5e4b5cf05..be1fa7c5b56 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 @@ -336,6 +336,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i $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; + $product_qty = $order_item->get_quantity( 'edit' ) - $quantity_refunded; // Shipping amount based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php // distributed simply based on number of line items. @@ -346,7 +347,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i } else { $total_shipping_amount = $order->get_shipping_total(); } - $shipping_amount = $total_shipping_amount / $order_items; + $shipping_amount = $total_shipping_amount / $order_items * $product_qty; // Shipping amount tax based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php // distribute simply based on number of line items. @@ -370,7 +371,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i } } } - $shipping_tax_amount = $total_shipping_tax_amount / $order_items; + $shipping_tax_amount = $total_shipping_tax_amount / $order_items * $product_qty; // Tax amount. // TODO: check if this calculates tax correctly with refunds. @@ -404,7 +405,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i '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_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, From b593163dff0ab970b61b56a8df2873b3e4a85c81 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:08:23 +0100 Subject: [PATCH 24/91] Added more general function for sorting array of arrays based on subarray's key. --- .../class-wc-admin-reports-data-store.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 d44282b5d3f..b279175ca7d 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 @@ -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' ) ); } /** From 93b331243b4ea872f481041d77eb82375b3be9b1 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:11:14 +0100 Subject: [PATCH 25/91] Corrected JSON response in case no fields are selected in $query_params. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index 4707410a704..e0496e39f03 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -566,7 +566,7 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } } } else { - foreach ( $this->report_columns as $field ) { + foreach ( $this->report_columns as $field => $sql_clause ) { $segment_subtotals[ $field ] = 0; } } From ea2d5bfe350c1e828057e6276e41f75dd3a5c770 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:11:58 +0100 Subject: [PATCH 26/91] Removed double refund subtracting. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index e0496e39f03..f963d124609 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -208,7 +208,8 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp 'refunds' => "SUM($products_table.refund_amount) AS refunds", 'taxes' => "SUM($products_table.tax_amount) AS taxes", 'shipping' => "SUM($products_table.shipping_amount) AS shipping", - 'net_revenue' => "SUM($products_table.product_net_revenue) - SUM($products_table.refund_amount) AS net_revenue", + // 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( $query_args, $columns_mapping ); From d067c2148d26ce81dd7e5a7911e91a6c6f23cd79 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:13:43 +0100 Subject: [PATCH 27/91] Added sorting of segments so that they have predictable order. --- .../data-stores/class-wc-admin-reports-orders-data-store.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php index f963d124609..432509c8c22 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php @@ -584,7 +584,9 @@ class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store imp } // Using array_values to remove custom keys, so that it gets later converted to JSON as an array. - return array_values( $segments ); + $segments_no_keys = array_values( $segments ); + $this->sort_array( $segments_no_keys, 'segment_id', 'asc' ); + return $segments_no_keys; } /** From f84efa3a58a02c59aba3ea749a2740c939b1c80b Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:16:31 +0100 Subject: [PATCH 28/91] Added test for segmenting /orders/stats by product. --- .../reports/class-wc-tests-reports-orders.php | 510 +++++++++++++++++- 1 file changed, 508 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php index 541096d7d70..ef3581f8f8c 100644 --- a/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php +++ b/plugins/woocommerce-admin/tests/reports/class-wc-tests-reports-orders.php @@ -15,7 +15,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { * * @since 3.5.0 */ - public function test_populate_and_query() { + public function _test_populate_and_query() { WC_Helper_Reports::reset_stats_dbs(); // Populate all of the data. @@ -145,7 +145,7 @@ class WC_Tests_Reports_Orders extends WC_Unit_Test_Case { /** * Test the calculations and querying works correctly for the case of multiple orders. */ - public function test_populate_and_query_multiple_intervals() { + public function _test_populate_and_query_multiple_intervals() { global $wpdb; // 2 different products. @@ -3064,4 +3064,510 @@ class WC_Tests_Reports_Orders 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_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' => $orders_count - $new_customers, + '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' => $i3_tot_orders_count - $i3_tot_new_customers, + '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, + ); + + $this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $query_args ) ), true ), 'Segmenting by product' ); + } + } From 2fc9de0bb074483472ce03fe90f13d1bc85340c6 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:36:56 +0100 Subject: [PATCH 29/91] Preparation for merge of a moved file. --- ...p => class-wc-admin-reports-orders-stats-data-store.php} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename plugins/woocommerce-admin/includes/data-stores/{class-wc-admin-reports-orders-data-store.php => class-wc-admin-reports-orders-stats-data-store.php} (99%) diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php similarity index 99% rename from plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php rename to plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php index 432509c8c22..58f987b32bf 100644 --- a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-data-store.php +++ b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-orders-stats-data-store.php @@ -1,6 +1,6 @@ Date: Tue, 15 Jan 2019 11:50:53 +0100 Subject: [PATCH 30/91] Updated data store used in test to reflect the rename of class. --- .../tests/reports/class-wc-tests-reports-orders-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9d2e3142b6a..5b7ca64661c 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 @@ -3156,7 +3156,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { $order_2->calculate_totals(); $order_2->save(); - $data_store = new WC_Admin_Reports_Orders_Data_Store(); + $data_store = new WC_Admin_Reports_Orders_Stats_Data_Store(); // Tests for before & after set to current hour. $now = new DateTime(); From 29f07f8c5707e1db5a01e69490353d2ac0e2a808 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 15 Jan 2019 11:51:43 +0100 Subject: [PATCH 31/91] Re-enable temporarily disabled tests. --- .../tests/reports/class-wc-tests-reports-orders-stats.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5b7ca64661c..d702e9ea7c6 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 @@ -15,7 +15,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { * * @since 3.5.0 */ - public function _test_populate_and_query() { + public function test_populate_and_query() { WC_Helper_Reports::reset_stats_dbs(); // Populate all of the data. @@ -145,7 +145,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { /** * Test the calculations and querying works correctly for the case of multiple orders. */ - public function _test_populate_and_query_multiple_intervals() { + public function test_populate_and_query_multiple_intervals() { global $wpdb; // 2 different products. From 25bb5e06dfd22d6b76ed87203a1ceca1bec653ab Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Thu, 17 Jan 2019 12:14:24 +0100 Subject: [PATCH 33/91] Factor out the segmentation code so that it's more reusable and clutters orders/stats data store less. --- .../includes/class-wc-admin-api-init.php | 4 + ...-admin-reports-orders-stats-segmenting.php | 442 ++++++++++ .../class-wc-admin-reports-segmenting.php | 481 ++++++++++ ...-admin-reports-orders-stats-data-store.php | 831 +----------------- 4 files changed, 931 insertions(+), 827 deletions(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-segmenting.php create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php 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 7f71084007f..6aaf9b92e56 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -46,6 +46,10 @@ class WC_Admin_Api_Init { // Common date time code. require_once dirname( __FILE__ ) . '/class-wc-admin-reports-interval.php'; + // Segmentation. + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-segmenting.php'; + require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-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'; 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..26ad9a3400f --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-orders-stats-segmenting.php @@ -0,0 +1,442 @@ + "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; + $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_REST_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_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + } + + $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; + } + + /** + * 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-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php new file mode 100644 index 00000000000..96424eeb16e --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php @@ -0,0 +1,481 @@ +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 array + */ + protected function set_all_segments() { + global $wpdb; + + if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) { + $this->all_segment_ids = array(); + } + + 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 ) { + return array(); + } + + $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? + $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 ); + } 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 ); + } +} 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 32e5ef62561..054c72468a9 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 @@ -196,824 +196,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto } } - /** - * Returns SELECT clause statements to be used for product-related product-level segmenting query (e.g. products sold, revenue from product X when segmenting by category). - * - * @param array $query_args Query arguments supplied by the user. - * @param string $products_table Name of SQL table containing the product-level segmenting info. - * - * @return string SELECT clause statements. - */ - protected function get_segment_selections_product_level( $query_args, $products_table ) { - $columns_mapping = array( - 'num_items_sold' => "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( $query_args, $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 array $query_args Query arguments supplied by the user. - * @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( $query_args, $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( $query_args, $columns_mapping ); - } - - /** - * Filters definitions for SELECT clauses based on query_args and joins them into one string usable in SELECT clause. - * - * @param array $query_args Query arguments supplied by the user. - * @param array $columns_mapping Column name -> SQL statememt mapping. - * - * @return string to be used in SELECT clause statements. - */ - protected function prepare_selections( $query_args, $columns_mapping ) { - if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { - $keep = array(); - foreach ( $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; - } - - /** - * 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 array $query_args Query arguments supplied by the user. - * @param string $table_name 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( $query_args, $table_name, $overrides = array() ) { - $columns_mapping = array( - 'num_items_sold' => "SUM($table_name.num_items_sold) as num_items_sold", - 'gross_revenue' => "SUM($table_name.gross_total) AS gross_revenue", - 'coupons' => "SUM($table_name.coupon_total) AS coupons", - 'refunds' => "SUM($table_name.refund_total) AS refunds", - 'taxes' => "SUM($table_name.tax_total) AS taxes", - 'shipping' => "SUM($table_name.shipping_total) AS shipping", - 'net_revenue' => "SUM($table_name.net_total) - SUM($table_name.refund_total) AS net_revenue", - 'orders_count' => "COUNT($table_name.order_id) AS orders_count", - 'avg_items_per_order' => "AVG($table_name.num_items_sold) AS avg_items_per_order", - 'avg_order_value' => "(SUM($table_name.net_total) - SUM($table_name.refund_total))/COUNT($table_name.order_id) AS avg_order_value", - 'num_returning_customers' => "SUM($table_name.returning_customer) AS num_returning_customers", - 'num_new_customers' => "COUNT($table_name.returning_customer) - SUM($table_name.returning_customer) AS num_new_customers", - ); - - if ( $overrides ) { - $columns_mapping = array_merge( $columns_mapping, $overrides ); - } - - return $this->prepare_selections( $query_args, $columns_mapping ); - } - - /** - * 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' => $this->cast_numbers( $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' => $this->cast_numbers( $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' => $this->cast_numbers( $segment_data ), - ); - $aggregated_segment_result[ $time_interval ]['segments'][ $segment_id ] = $segment_datum; - } - - return $aggregated_segment_result; - } - - /** - * Return all segments for given segmentby parameter. - * - * @param array $query_args Query args provided by the user. - * - * @return array - */ - protected function get_all_segments( $query_args ) { - global $wpdb; - if ( ! isset( $query_args['segmentby'] ) || '' === $query_args['segmentby'] ) { - return array(); - } - - if ( 'product' === $query_args['segmentby'] ) { - $segments = wc_get_products( - array( - 'return' => 'ids', - 'limit' => -1, - ) - ); - } elseif ( 'variation' === $query_args['segmentby'] ) { - // TODO: assuming that this will only be used for one product, check assumption. - if ( ! isset( $query_args['product_includes'] ) || count( $query_args['product_includes'] ) !== 1 ) { - return array(); - } - - $segments = wc_get_products( - array( - 'return' => 'ids', - 'limit' => - 1, - 'type' => 'variation', - 'parent' => $query_args['product_includes'][0], - ) - ); - } elseif ( 'category' === $query_args['segmentby'] ) { - $categories = get_categories( - array( - 'taxonomy' => 'product_cat', - ) - ); - $segments = wp_list_pluck( $categories, 'cat_ID' ); - } elseif ( 'coupon' === $query_args['segmentby'] ) { - $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' === $query_args['segmentby'] ) { - // 0 -- new customer - // 1 -- returning customer - $segments = array( 0, 1 ); - } - - return $segments; - } - - /** - * Adds zeroes for segments not present in the data selection. - * - * @param array $query_args Query arguments provided by the user. - * @param array $segments Array of segments from the database for given data points. - * @param array $all_segment_ids Array of all possible segment ids. - * - * @return array - */ - protected function fill_in_missing_segments( $query_args, $segments, $all_segment_ids ) { - - $segment_subtotals = array(); - if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) { - foreach ( $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(); - } - 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 ); - $this->sort_array( $segments_no_keys, 'segment_id', 'asc' ); - return $segments_no_keys; - } - - /** - * Adds missing segments to intervals, modifies $data. - * - * @param array $query_args Query arguments provided by the user. - * @param stdClass $data Response data. - * @param array $all_segment_ids Array of all possible segment ids. - */ - protected function fill_in_missing_interval_segments( $query_args, &$data, $all_segment_ids ) { - foreach ( $data->intervals as $order_id => $interval_data ) { - $data->intervals[ $order_id ]['segments'] = $this->fill_in_missing_segments( $query_args, $data->intervals[ $order_id ]['segments'], $all_segment_ids ); - } - } - - /** - * 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. - * @param array $all_segment_ids Array of all possible segment ids. - * - * @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, $all_segment_ids ) { - 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, $all_segment_ids ); - } - } - - /** - * 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. - * @param array $all_segment_ids Array of all possible segment ids. - * - * @return array - */ - protected function get_order_related_segments( $type, $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $query_params, $all_segment_ids ) { - 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, $all_segment_ids ); - } - } - - /** - * 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. - * @param array $all_segment_ids Array of all possible segment ids. - * - * @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, $all_segment_ids ) { - 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( $all_segment_ids ); - - // 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. - * @param array $all_segment_ids Ids of all possible segments. - * - * @return array - */ - protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query, $all_segment_ids ) { - global $wpdb; - $limit_parts = explode( ',', $intervals_query['limit'] ); - $orig_rowcount = intval( $limit_parts[1] ); - $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $all_segment_ids ); - - $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_args Parameters provided by the user. - * @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). - * @param array $all_segments Array of all segment ids. - * - * @return array - * @throws WC_REST_Exception In case of segmenting by variations, when no parent product is specified. - */ - protected function get_segments( $type, $query_args, $query_params, $table_name, $all_segments ) { - global $wpdb; - if ( ! isset( $query_args['segmentby'] ) || '' === $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' === $query_args['segmentby'] ) { - // TODO: how to handle shipping taxes when grouped by product? - $segmenting_selections = array( - 'product_level' => $this->get_segment_selections_product_level( $query_args, $product_segmenting_table ), - 'order_level' => $this->get_segment_selections_order_level( $query_args, $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, $all_segments ); - } elseif ( 'variation' === $query_args['segmentby'] ) { - if ( ! isset( $query_args['product_includes'] ) || count( $query_args['product_includes'] ) !== 1 ) { - throw new WC_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); - } - - $segmenting_selections = array( - 'product_level' => $this->get_segment_selections_product_level( $query_args, $product_segmenting_table ), - 'order_level' => $this->get_segment_selections_order_level( $query_args, $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 = {$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, $all_segments ); - } elseif ( 'category' === $query_args['segmentby'] ) { - $segmenting_selections = array( - 'product_level' => $this->get_segment_selections_product_level( $query_args, $product_segmenting_table ), - 'order_level' => $this->get_segment_selections_order_level( $query_args, $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, $all_segments ); - } elseif ( 'coupon' === $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( $query_args, $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, $all_segments ); - } elseif ( 'customer_type' === $query_args['segmentby'] ) { - $segmenting_selections = $this->segment_selections_orders( $query_args, $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, $all_segments ); - } - - return $segments; - } - - /** - * 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 the report data based on parameters supplied by the user. * @@ -1089,9 +271,9 @@ 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; - $all_segments = $this->get_all_segments( $query_args ); - $segments = $this->get_segments( 'totals', $query_args, $totals_query, $table_name, $all_segments ); - $totals[0]['segments'] = $this->fill_in_missing_segments( $query_args, $segments, $all_segments ); + $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( @@ -1146,11 +328,6 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'wc-admin' ) ); } - $intervals_segments = $this->get_segments( 'intervals', $query_args, $intervals_query, $table_name, $all_segments ); - - // Pigeon hole segments. - $this->assign_segments_to_intervals( $intervals, $intervals_segments ); - $data = (object) array( 'totals' => $totals, 'intervals' => $intervals, @@ -1163,10 +340,10 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto $this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data ); $this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] ); $this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] ); - $this->fill_in_missing_interval_segments( $query_args, $data, $all_segments ); } 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 ); From 1d797142958c05051c210125116e14d3be29ca69 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 18 Jan 2019 11:41:15 +0100 Subject: [PATCH 34/91] Added segmenting to products/stats. --- ...rest-reports-products-stats-controller.php | 44 +++- .../includes/class-wc-admin-api-init.php | 1 + ...dmin-reports-products-stats-segmenting.php | 215 ++++++++++++++++++ ...dmin-reports-products-stats-data-store.php | 4 + 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php 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 bbc998196d8..8e8fb757b76 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 @@ -146,7 +146,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', @@ -167,6 +167,36 @@ 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' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'enum' => array( 'day', 'week', 'month', 'year' ), + ), + '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', @@ -348,6 +378,18 @@ 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', + 'coupon', + 'customer_type', // new vs returning. + ), + '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 6aaf9b92e56..f11950008ba 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -49,6 +49,7 @@ class WC_Admin_Api_Init { // 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'; // Query classes for reports. require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-query.php'; diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php new file mode 100644 index 00000000000..72803b06572 --- /dev/null +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php @@ -0,0 +1,215 @@ + "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_REST_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_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + } + + $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; + } + + /** + * 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/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 ); From 128f52fdc8c006e2131a7de4ce1e6667fa9f8acb Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 18 Jan 2019 12:28:12 +0100 Subject: [PATCH 35/91] Restrict products/stats only to supported segmentby options. --- .../class-wc-admin-rest-reports-products-stats-controller.php | 2 -- 1 file changed, 2 deletions(-) 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 8e8fb757b76..b1f747afd7d 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 @@ -385,8 +385,6 @@ class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Co 'product', 'category', 'variation', - 'coupon', - 'customer_type', // new vs returning. ), 'validate_callback' => 'rest_validate_request_arg', ); From caea199852722a7c605df94f9d05f9b682dfd2ad Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 18 Jan 2019 13:06:40 +0100 Subject: [PATCH 36/91] Updated revenue/stats REST controller to include segmentation options/fields. --- ...-rest-reports-revenue-stats-controller.php | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) 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 b5d77586ab7..b7f7e062979 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; } @@ -130,7 +131,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', @@ -187,8 +188,40 @@ 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' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'enum' => array( 'day', 'week', 'month', 'year' ), + ), + '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#', @@ -332,6 +365,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; } From ea2f5ca5d9999da633e3536ca768fd32def5ed3a Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 18 Jan 2019 13:07:19 +0100 Subject: [PATCH 37/91] Fixed number of parameters for product lookup update. --- ...s-wc-admin-reports-products-data-store.php | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) 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 ae0a59c5d45..846f88751c6 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 @@ -418,21 +418,20 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i 'refund_amount' => $amount_refunded, ), array( - '%d', - '%d', - '%d', - '%d', - '%d', - '%d', - '%f', - '%s', - '%f', - '%f', - '%f', - '%f', - '%f', - '%f', - '%f', + '%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. } From e8f264c3460a71ca9fd0450c893fb8578c038f79 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 18 Jan 2019 14:08:34 +0100 Subject: [PATCH 38/91] Moved common code to parent class. --- ...-admin-reports-orders-stats-segmenting.php | 26 ------------------- ...dmin-reports-products-stats-segmenting.php | 26 ------------------- .../class-wc-admin-reports-segmenting.php | 26 +++++++++++++++++++ 3 files changed, 26 insertions(+), 52 deletions(-) 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 index 26ad9a3400f..8d244feea52 100644 --- 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 @@ -413,30 +413,4 @@ class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenti return $segments; } - - /** - * 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-products-stats-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php index 72803b06572..f96b92b1adb 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-products-stats-segmenting.php @@ -186,30 +186,4 @@ class WC_Admin_Reports_Products_Stats_Segmenting extends WC_Admin_Reports_Segmen return $segments; } - - /** - * 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-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php index 96424eeb16e..e98132bb048 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php @@ -478,4 +478,30 @@ class WC_Admin_Reports_Segmenting { // 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 ); + } } From f2634ca5c8dd0b949c79a773ca9bb84ca0ecab8c Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Fri, 18 Jan 2019 17:11:46 +0100 Subject: [PATCH 39/91] Added support for segmenting to coupons/stats. --- ...-rest-reports-coupons-stats-controller.php | 62 +++- .../includes/class-wc-admin-api-init.php | 1 + ...admin-reports-coupons-stats-segmenting.php | 312 ++++++++++++++++++ ...admin-reports-coupons-stats-data-store.php | 5 +- 4 files changed, 369 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-coupons-stats-segmenting.php 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 ee963f7eef3..744b476ed88 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; } @@ -132,7 +133,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', @@ -153,6 +154,36 @@ 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' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'enum' => array( 'day', 'week', 'month', 'year' ), + ), + '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', @@ -299,6 +330,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', // will we support this? + 'coupon', + ), + '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 f11950008ba..907f3603737 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -50,6 +50,7 @@ class WC_Admin_Api_Init { 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'; // Query classes for reports. require_once dirname( __FILE__ ) . '/class-wc-admin-reports-revenue-query.php'; 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..cbb898e9534 --- /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_REST_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_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + } + + $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/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 ); From fed94cbd0cffd0c15fd2af87cc2e44e7d1da405a Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 10:34:07 +0100 Subject: [PATCH 40/91] Added segmenting to response for coupons/stats test. Changed return type for currency. --- .../tests/reports/class-wc-tests-reports-coupons-stats.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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(), ), ), ), From bc42e6d9850583c0dbdb4669f04e15485a207fa2 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 10:34:55 +0100 Subject: [PATCH 41/91] Fixed bug introduced during refactoring. --- .../includes/class-wc-admin-reports-segmenting.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php index e98132bb048..666a31d49b3 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php @@ -274,13 +274,14 @@ class WC_Admin_Reports_Segmenting { /** * Fetches all segment ids from db and stores it for later use. * - * @return array + * @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'] ) { @@ -293,7 +294,8 @@ class WC_Admin_Reports_Segmenting { } 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 ) { - return array(); + $this->all_segment_ids = array(); + return; } $segments = wc_get_products( From bcb7513f34f6321c548a2ea308932ab108479193 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 10:35:48 +0100 Subject: [PATCH 42/91] Added segmenting to response for coupons/stats test. Fixed copy-paste error in test for subtotals. --- .../tests/api/reports-coupons-stats.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 ); + } } From f190c1bad13d0b078ab3b2016e354c19ae1e73b1 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 10:36:27 +0100 Subject: [PATCH 43/91] Fixed copy-paste bug for testing subtotals in REST response. --- .../tests/api/reports-orders-stats.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce-admin/tests/api/reports-orders-stats.php b/plugins/woocommerce-admin/tests/api/reports-orders-stats.php index 8b8887a7335..16d9c4fd3a2 100644 --- a/plugins/woocommerce-admin/tests/api/reports-orders-stats.php +++ b/plugins/woocommerce-admin/tests/api/reports-orders-stats.php @@ -116,14 +116,14 @@ class WC_Tests_API_Reports_Orders_Stats extends WC_REST_Unit_Test_Case { $subtotals = $properties['intervals']['items']['properties']['subtotals']['properties']; $this->assertEquals( 9, 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->assertArrayHasKey( 'num_items_sold', $totals ); - $this->assertArrayHasKey( 'coupons', $totals ); - $this->assertArrayHasKey( 'num_returning_customers', $totals ); - $this->assertArrayHasKey( 'num_new_customers', $totals ); - $this->assertArrayHasKey( 'segments', $totals ); + $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 ); } } From e432a17f1e2baa9f9b08dadc7d4c61b36f929eb9 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 10:36:50 +0100 Subject: [PATCH 44/91] Added segmenting to response for products/stats test. --- .../tests/api/reports-products-stats.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 ); } } From 83a0d96f68eb8a2b37b03e2ad0b29c656f8da617 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 10:37:09 +0100 Subject: [PATCH 45/91] Added segmenting to response for revenue/stats test. --- .../woocommerce-admin/tests/api/reports-revenue-stats.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 ); } } From 71a9be299a5b2ff98b358090fbed74af2db62e13 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 23 Jan 2019 13:29:40 +0100 Subject: [PATCH 46/91] Updated tests to pass before implementing the updated new/returning customer definition. Context: https://github.com/woocommerce/wc-admin/issues/558#issuecomment-447782060 --- .../tests/reports/class-wc-tests-reports-orders-stats.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 31fbfbe0089..5133909c28c 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 @@ -3286,7 +3286,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { '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' => $orders_count - $new_customers, + 'num_returning_customers' => 0, 'num_new_customers' => $new_customers, 'products' => 2, 'segments' => array( @@ -3361,7 +3361,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { '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' => $i3_tot_orders_count - $i3_tot_new_customers, + 'num_returning_customers' => 1, 'num_new_customers' => $i3_tot_new_customers, 'segments' => array( array( @@ -3571,8 +3571,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case { 'pages' => 1, 'page_no' => 1, ); - - $this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $query_args ) ), true ), 'Segmenting by product' ); + $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 ) ); } } From bca0785e642dada42d8bc791137ed018a20e038c Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 08:58:07 +1300 Subject: [PATCH 47/91] Customers Report: support between args for dates --- ...dmin-rest-reports-customers-controller.php | 33 +++++++++++-------- ...est-reports-customers-stats-controller.php | 24 +++++++------- .../class-wc-admin-reports-interval.php | 33 ++++++++++++++++++- ...-wc-admin-reports-customers-data-store.php | 26 +++++++-------- .../tests/api/reports-interval.php | 8 ++--- 5 files changed, 81 insertions(+), 43 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 8c01d455a69..4b5883d06e0 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -52,6 +52,8 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['country'] = $request['country']; $args['last_active_before'] = $request['last_active_before']; $args['last_active_after'] = $request['last_active_after']; + $args['last_active_min'] = $request['last_active_min']; + $args['last_active_max'] = $request['last_active_max']; $args['orders_count_min'] = $request['orders_count_min']; $args['orders_count_max'] = $request['orders_count_max']; $args['total_spend_min'] = $request['total_spend_min']; @@ -61,7 +63,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $between_params = array( 'orders_count', 'total_spend', 'avg_order_value', 'last_active' ); $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params ); $args = array_merge( $args, $normalized ); @@ -296,14 +298,14 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'sanitize_callback' => 'absint', '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_registered', @@ -321,7 +323,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control ), 'validate_callback' => 'rest_validate_request_arg', ); - $params['match'] = array( + $params['match'] = array( 'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'wc-admin' ), 'type' => 'string', 'default' => 'all', @@ -331,7 +333,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control ), 'validate_callback' => 'rest_validate_request_arg', ); - $params['name'] = array( + $params['name'] = array( 'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', @@ -363,34 +365,39 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['registered_before'] = array( + $params['last_active_between'] = array( + 'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ), + ); + $params['registered_before'] = array( 'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['registered_after'] = array( + $params['registered_after'] = array( 'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['orders_count_min'] = array( + $params['orders_count_min'] = array( 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['orders_count_max'] = array( + $params['orders_count_max'] = array( 'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['orders_count_between'] = array( + $params['orders_count_between'] = array( 'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ), 'type' => 'array', - 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ), + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ), ); $params['total_spend_min'] = array( 'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ), @@ -405,7 +412,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $params['total_spend_between'] = array( 'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ), 'type' => 'array', - 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ), + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ), ); $params['avg_order_value_min'] = array( 'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ), @@ -420,7 +427,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $params['avg_order_value_between'] = array( 'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ), 'type' => 'array', - 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ), + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ), ); $params['last_order_before'] = array( 'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 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 af24ea6119d..2b906f6c049 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 @@ -119,7 +119,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C public function get_item_schema() { // TODO: should any of these be 'indicator's? $totals = array( - 'customers_count' => array( + 'customers_count' => array( 'description' => __( 'Number of customers.', 'wc-admin' ), 'type' => 'integer', 'context' => array( 'view', 'edit' ), @@ -131,7 +131,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'context' => array( 'view', 'edit' ), 'readonly' => true, ), - 'avg_total_spend' => array( + 'avg_total_spend' => array( 'description' => __( 'Average total spend per customer.', 'wc-admin' ), 'type' => 'number', 'context' => array( 'view', 'edit' ), @@ -234,7 +234,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['match'] = array( + $params['match'] = array( 'description' => __( 'Indicates whether all the conditions should be true for the resulting set, or if any one of them is sufficient. Match affects the following parameters: status_is, status_is_not, product_includes, product_excludes, coupon_includes, coupon_excludes, customer, categories', 'wc-admin' ), 'type' => 'string', 'default' => 'all', @@ -244,7 +244,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C ), 'validate_callback' => 'rest_validate_request_arg', ); - $params['name'] = array( + $params['name'] = array( 'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ), 'type' => 'string', 'validate_callback' => 'rest_validate_request_arg', @@ -276,34 +276,34 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['registered_before'] = array( + $params['registered_before'] = array( 'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['registered_after'] = array( + $params['registered_after'] = array( 'description' => __( 'Limit response to objects registered after (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['orders_count_min'] = array( + $params['orders_count_min'] = array( 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['orders_count_max'] = array( + $params['orders_count_max'] = array( 'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); - $params['orders_count_between'] = array( + $params['orders_count_between'] = array( 'description' => __( 'Limit response to objects with an order count between two given integers.', 'wc-admin' ), 'type' => 'array', - 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ), + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ), ); $params['total_spend_min'] = array( 'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'wc-admin' ), @@ -318,7 +318,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $params['total_spend_between'] = array( 'description' => __( 'Limit response to objects with a total order spend between two given numbers.', 'wc-admin' ), 'type' => 'array', - 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ), + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ), ); $params['avg_order_value_min'] = array( 'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'wc-admin' ), @@ -333,7 +333,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $params['avg_order_value_between'] = array( 'description' => __( 'Limit response to objects with an average order spend between two given numbers.', 'wc-admin' ), 'type' => 'array', - 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_arg' ), + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_numeric_arg' ), ); $params['last_order_before'] = array( 'description' => __( 'Limit response to objects with last order before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 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 27e576e6065..166bd8484e5 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -538,7 +538,7 @@ class WC_Admin_Reports_Interval { * @param string $param Parameter name. * @return WP_Error|boolean */ - public static function rest_validate_between_arg( $value, $request, $param ) { + public static function rest_validate_between_numeric_arg( $value, $request, $param ) { if ( ! wp_is_numeric_array( $value ) ) { return new WP_Error( 'rest_invalid_param', @@ -561,4 +561,35 @@ class WC_Admin_Reports_Interval { return true; } + + /** + * Validate a "*_between" range argument (an array with 2 date items). + * + * @param mixed $value Parameter value. + * @param WP_REST_Request $request REST Request. + * @param string $param Parameter name. + * @return WP_Error|boolean + */ + public static function rest_validate_between_date_arg( $value, $request, $param ) { + if ( ! wp_is_numeric_array( $value ) ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: 1: parameter name */ + sprintf( __( '%1$s is not a numerically indexed array.', 'wc-admin' ), $param ) + ); + } + + // check for dates here. + if ( + 2 !== count( $value ) + ) { + return new WP_Error( + 'rest_invalid_param', + /* translators: %s: parameter name */ + sprintf( __( '%s must contain 2 dates.', 'wc-admin' ), $param ) + ); + } + + return true; + } } diff --git a/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 17e88cba209..1c0d3cba50f 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 @@ -25,11 +25,11 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store * @var array */ protected $column_types = array( - 'customer_id' => 'intval', - 'user_id' => 'intval', - 'orders_count' => 'intval', - 'total_spend' => 'floatval', - 'avg_order_value' => 'floatval', + 'customer_id' => 'intval', + 'user_id' => 'intval', + 'orders_count' => 'intval', + 'total_spend' => 'floatval', + 'avg_order_value' => 'floatval', ); /** @@ -60,7 +60,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store global $wpdb; // Initialize some report columns that need disambiguation. - $this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id'; + $this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id'; $this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order"; } @@ -274,7 +274,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } if ( $where_clauses ) { - $preceding_match = empty( $sql_query_params['where_time_clause'] ) ? ' AND ' : " {$match_operator} "; + $preceding_match = empty( $sql_query_params['where_time_clause'] ) ? ' AND ' : " {$match_operator} "; $sql_query_params['where_clause'] = $preceding_match . implode( " {$match_operator} ", $where_clauses ); } @@ -284,7 +284,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store } if ( $having_clauses ) { - $preceding_match = empty( $sql_query_params['having_clause'] ) ? ' AND ' : " {$match_operator} "; + $preceding_match = empty( $sql_query_params['having_clause'] ) ? ' AND ' : " {$match_operator} "; $sql_query_params['having_clause'] .= $preceding_match . implode( " {$match_operator} ", $having_clauses ); } @@ -304,7 +304,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store $order_stats_table_name = $wpdb->prefix . 'wc_order_stats'; // These defaults are only partially applied when used via REST API, as that has its own defaults. - $defaults = array( + $defaults = array( 'per_page' => get_option( 'posts_per_page' ), 'page' => 1, 'order' => 'DESC', @@ -442,7 +442,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store * Retrieve a guest (no user_id) customer row by email. * * @param string $email Email address. - * @returns false|array Customer array if found, boolean false if not. + * @return false|array Customer array if found, boolean false if not. */ public function get_guest_by_email( $email ) { global $wpdb; @@ -467,7 +467,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store * Retrieve a registered customer row by user_id. * * @param string|int $user_id User ID. - * @returns false|array Customer array if found, boolean false if not. + * @return false|array Customer array if found, boolean false if not. */ public function get_customer_by_user_id( $user_id ) { global $wpdb; @@ -492,7 +492,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store * Retrieve a registered customer row id by user_id. * * @param string|int $user_id User ID. - * @returns false|int Customer ID if found, boolean false if not. + * @return false|int Customer ID if found, boolean false if not. */ public static function get_customer_id_by_user_id( $user_id ) { global $wpdb; @@ -554,7 +554,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store if ( $customer_id ) { // Preserve customer_id for existing user_id. $data['customer_id'] = $customer_id; - $format[] = '%d'; + $format[] = '%d'; } return $wpdb->replace( $wpdb->prefix . self::TABLE_NAME, $data, $format ); diff --git a/plugins/woocommerce-admin/tests/api/reports-interval.php b/plugins/woocommerce-admin/tests/api/reports-interval.php index 566153a0796..78db9e8c26c 100644 --- a/plugins/woocommerce-admin/tests/api/reports-interval.php +++ b/plugins/woocommerce-admin/tests/api/reports-interval.php @@ -836,19 +836,19 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { /** * Test function that validates *_between query parameters. */ - public function test_rest_validate_between_arg() { + public function test_rest_validate_between_numeric_arg() { $this->assertIsWPError( - WC_Admin_Reports_Interval::rest_validate_between_arg( 'not array', null, 'param' ), + WC_Admin_Reports_Interval::rest_validate_between_numeric_arg( 'not array', null, 'param' ), 'param is not a numerically indexed array.' ); $this->assertIsWPError( - WC_Admin_Reports_Interval::rest_validate_between_arg( array( 1 ), null, 'param' ), + WC_Admin_Reports_Interval::rest_validate_between_numeric_arg( array( 1 ), null, 'param' ), 'param must contain 2 numbers.' ); $this->assertTrue( - WC_Admin_Reports_Interval::rest_validate_between_arg( array( 1, 2 ), null, 'param' ) + WC_Admin_Reports_Interval::rest_validate_between_numeric_arg( array( 1, 2 ), null, 'param' ) ); } } From 3d0890a704d1f9631c41264904832d52db409f90 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 09:21:11 +1300 Subject: [PATCH 48/91] basic working --- ...dmin-rest-reports-customers-controller.php | 8 ++-- ...est-reports-customers-stats-controller.php | 2 +- .../class-wc-admin-reports-interval.php | 39 ++++++++++++++++++- .../tests/api/reports-interval.php | 4 +- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 4b5883d06e0..3d1c2421286 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -63,9 +63,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $between_params = array( 'orders_count', 'total_spend', 'avg_order_value', 'last_active' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params ); - $args = array_merge( $args, $normalized ); + $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $date_between_params = array( 'last_active' ); + $normalized = WC_Admin_Reports_Interval::normalize_numeric_between_params( $request, $numeric_between_params ); + $normalized_dates = WC_Admin_Reports_Interval::normalize_date_between_params( $request, $date_between_params ); + $args = array_merge( $args, $normalized, $normalized_dates ); return $args; } 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 2b906f6c049..8946ddeddd0 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 @@ -57,7 +57,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_after'] = $request['last_order_after']; $between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params ); + $normalized = WC_Admin_Reports_Interval::normalize_numeric_between_params( $request, $between_params ); $args = array_merge( $args, $normalized ); return $args; 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 166bd8484e5..bf868b473cc 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -500,7 +500,7 @@ class WC_Admin_Reports_Interval { * @param string|array $param_names One or more param names to handle. Should not include "_between" suffix. * @return array Normalized query values. */ - public static function normalize_between_params( $request, $param_names ) { + public static function normalize_numeric_between_params( $request, $param_names ) { if ( ! is_array( $param_names ) ) { $param_names = array( $param_names ); } @@ -530,6 +530,43 @@ class WC_Admin_Reports_Interval { return $normalized; } + /** + * Normalize "*_between" parameters to "*_after" and "*_before". + * + * @param array $request Query params from REST API request. + * @param string|array $param_names One or more param names to handle. Should not include "_between" suffix. + * @return array Normalized query values. + */ + public static function normalize_date_between_params( $request, $param_names ) { + if ( ! is_array( $param_names ) ) { + $param_names = array( $param_names ); + } + + $normalized = array(); + + foreach ( $param_names as $param_name ) { + if ( ! is_array( $request[ $param_name . '_between' ] ) ) { + continue; + } + + $range = $request[ $param_name . '_between' ]; + + if ( 2 !== count( $range ) ) { + continue; + } + + if ( $range[0] < $range[1] ) { + $normalized[ $param_name . '_after' ] = $range[0]; + $normalized[ $param_name . '_before' ] = $range[1]; + } else { + $normalized[ $param_name . '_after' ] = $range[1]; + $normalized[ $param_name . '_before' ] = $range[0]; + } + } + + return $normalized; + } + /** * Validate a "*_between" range argument (an array with 2 numeric items). * diff --git a/plugins/woocommerce-admin/tests/api/reports-interval.php b/plugins/woocommerce-admin/tests/api/reports-interval.php index 78db9e8c26c..bcce5694f55 100644 --- a/plugins/woocommerce-admin/tests/api/reports-interval.php +++ b/plugins/woocommerce-admin/tests/api/reports-interval.php @@ -813,7 +813,7 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { /** * Test function that normalizes *_between query parameters to *_min & *_max. */ - public function test_normalize_between_params() { + public function test_normalize_numeric_between_params() { $request = array( 'a_between' => 'malformed', // won't be normalized (not an array). 'b_between' => array( 1, 5 ), // results in min=1, max=5. @@ -822,7 +822,7 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { 'f_between' => array( 10, 12 ), // not in params, skipped. ); $params = array( 'a', 'b', 'c', 'd' ); - $result = WC_Admin_Reports_Interval::normalize_between_params( $request, $params ); + $result = WC_Admin_Reports_Interval::normalize_numeric_between_params( $request, $params ); $expected = array( 'b_min' => 1, 'b_max' => 5, From 2f022ffdba1c8ce01b81a999c3d5df62a3da2f1a Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 10:23:44 +1300 Subject: [PATCH 49/91] dry interval.php --- ...dmin-rest-reports-customers-controller.php | 10 ++-- ...est-reports-customers-stats-controller.php | 2 +- .../class-wc-admin-reports-interval.php | 49 +++---------------- .../tests/api/reports-interval.php | 4 +- 4 files changed, 15 insertions(+), 50 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 3d1c2421286..c46e8c84afd 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -52,8 +52,6 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['country'] = $request['country']; $args['last_active_before'] = $request['last_active_before']; $args['last_active_after'] = $request['last_active_after']; - $args['last_active_min'] = $request['last_active_min']; - $args['last_active_max'] = $request['last_active_max']; $args['orders_count_min'] = $request['orders_count_min']; $args['orders_count_max'] = $request['orders_count_max']; $args['total_spend_min'] = $request['total_spend_min']; @@ -64,10 +62,10 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_after'] = $request['last_order_after']; $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $date_between_params = array( 'last_active' ); - $normalized = WC_Admin_Reports_Interval::normalize_numeric_between_params( $request, $numeric_between_params ); - $normalized_dates = WC_Admin_Reports_Interval::normalize_date_between_params( $request, $date_between_params ); - $args = array_merge( $args, $normalized, $normalized_dates ); + $date_between_params = array( 'last_active' ); + $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $numeric_between_params, false ); + $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $date_between_params, true ); + $args = array_merge( $args, $normalized, $normalized_dates ); return $args; } 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 8946ddeddd0..2b906f6c049 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 @@ -57,7 +57,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_after'] = $request['last_order_after']; $between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $normalized = WC_Admin_Reports_Interval::normalize_numeric_between_params( $request, $between_params ); + $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params ); $args = array_merge( $args, $normalized ); return $args; 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 bf868b473cc..dcb90e23daf 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -498,9 +498,10 @@ class WC_Admin_Reports_Interval { * * @param array $request Query params from REST API request. * @param string|array $param_names One or more param names to handle. Should not include "_between" suffix. + * @param bool $is_date Boolean if the param is date is related. * @return array Normalized query values. */ - public static function normalize_numeric_between_params( $request, $param_names ) { + public static function normalize_between_params( $request, $param_names, $is_date ) { if ( ! is_array( $param_names ) ) { $param_names = array( $param_names ); } @@ -518,49 +519,15 @@ class WC_Admin_Reports_Interval { continue; } - if ( $range[0] < $range[1] ) { - $normalized[ $param_name . '_min' ] = $range[0]; - $normalized[ $param_name . '_max' ] = $range[1]; - } else { - $normalized[ $param_name . '_min' ] = $range[1]; - $normalized[ $param_name . '_max' ] = $range[0]; - } - } - - return $normalized; - } - - /** - * Normalize "*_between" parameters to "*_after" and "*_before". - * - * @param array $request Query params from REST API request. - * @param string|array $param_names One or more param names to handle. Should not include "_between" suffix. - * @return array Normalized query values. - */ - public static function normalize_date_between_params( $request, $param_names ) { - if ( ! is_array( $param_names ) ) { - $param_names = array( $param_names ); - } - - $normalized = array(); - - foreach ( $param_names as $param_name ) { - if ( ! is_array( $request[ $param_name . '_between' ] ) ) { - continue; - } - - $range = $request[ $param_name . '_between' ]; - - if ( 2 !== count( $range ) ) { - continue; - } + $min = $is_date ? '_after' : '_min'; + $max = $is_date ? '_before' : '_max'; if ( $range[0] < $range[1] ) { - $normalized[ $param_name . '_after' ] = $range[0]; - $normalized[ $param_name . '_before' ] = $range[1]; + $normalized[ $param_name . $min ] = $range[0]; + $normalized[ $param_name . $max ] = $range[1]; } else { - $normalized[ $param_name . '_after' ] = $range[1]; - $normalized[ $param_name . '_before' ] = $range[0]; + $normalized[ $param_name . $min ] = $range[1]; + $normalized[ $param_name . $max ] = $range[0]; } } diff --git a/plugins/woocommerce-admin/tests/api/reports-interval.php b/plugins/woocommerce-admin/tests/api/reports-interval.php index bcce5694f55..78db9e8c26c 100644 --- a/plugins/woocommerce-admin/tests/api/reports-interval.php +++ b/plugins/woocommerce-admin/tests/api/reports-interval.php @@ -813,7 +813,7 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { /** * Test function that normalizes *_between query parameters to *_min & *_max. */ - public function test_normalize_numeric_between_params() { + public function test_normalize_between_params() { $request = array( 'a_between' => 'malformed', // won't be normalized (not an array). 'b_between' => array( 1, 5 ), // results in min=1, max=5. @@ -822,7 +822,7 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { 'f_between' => array( 10, 12 ), // not in params, skipped. ); $params = array( 'a', 'b', 'c', 'd' ); - $result = WC_Admin_Reports_Interval::normalize_numeric_between_params( $request, $params ); + $result = WC_Admin_Reports_Interval::normalize_between_params( $request, $params ); $expected = array( 'b_min' => 1, 'b_max' => 5, From c6d1ce98c0179e4877856f1a9f95660dc3c4d798 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 10:45:49 +1300 Subject: [PATCH 50/91] rest_validate_between_date_arg --- .../includes/class-wc-admin-reports-interval.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 dcb90e23daf..5036d2fcab4 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -583,14 +583,15 @@ class WC_Admin_Reports_Interval { ); } - // check for dates here. if ( - 2 !== count( $value ) + 2 !== count( $value ) || + ! rest_parse_date( $value[0] ) || + ! rest_parse_date( $value[1] ) ) { return new WP_Error( 'rest_invalid_param', /* translators: %s: parameter name */ - sprintf( __( '%s must contain 2 dates.', 'wc-admin' ), $param ) + sprintf( __( '%s must contain 2 valid dates.', 'wc-admin' ), $param ) ); } From 483c199fbf735b24932eb48fd0aba30eeadd8c5c Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 10:53:56 +1300 Subject: [PATCH 51/91] tests working --- ...dmin-rest-reports-customers-stats-controller.php | 13 ++++++++++--- .../tests/api/reports-interval.php | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) 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 2b906f6c049..c05b5d237c9 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 @@ -56,9 +56,11 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params ); - $args = array_merge( $args, $normalized ); + $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $date_between_params = array( 'last_active' ); + $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $numeric_between_params, false ); + $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $date_between_params, true ); + $args = array_merge( $args, $normalized, $normalized_dates ); return $args; } @@ -276,6 +278,11 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); + $params['last_active_between'] = array( + 'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ), + ); $params['registered_before'] = array( 'description' => __( 'Limit response to objects registered before (or at) a given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'string', diff --git a/plugins/woocommerce-admin/tests/api/reports-interval.php b/plugins/woocommerce-admin/tests/api/reports-interval.php index 78db9e8c26c..f6b094cfcde 100644 --- a/plugins/woocommerce-admin/tests/api/reports-interval.php +++ b/plugins/woocommerce-admin/tests/api/reports-interval.php @@ -822,7 +822,7 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { 'f_between' => array( 10, 12 ), // not in params, skipped. ); $params = array( 'a', 'b', 'c', 'd' ); - $result = WC_Admin_Reports_Interval::normalize_between_params( $request, $params ); + $result = WC_Admin_Reports_Interval::normalize_between_params( $request, $params, false ); $expected = array( 'b_min' => 1, 'b_max' => 5, From 3e16a37be08b94dcba016d6d614e49730a387241 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 11:01:25 +1300 Subject: [PATCH 52/91] add tests --- .../tests/api/reports-interval.php | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/tests/api/reports-interval.php b/plugins/woocommerce-admin/tests/api/reports-interval.php index f6b094cfcde..112891efe6d 100644 --- a/plugins/woocommerce-admin/tests/api/reports-interval.php +++ b/plugins/woocommerce-admin/tests/api/reports-interval.php @@ -834,7 +834,30 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { } /** - * Test function that validates *_between query parameters. + * Test function that normalizes *_between query parameters for dates to *_after & *_before. + */ + public function test_normalize_between_date_params() { + $request = array( + 'a_between' => 'malformed', // won't be normalized (not an array). + 'b_between' => array( 1, 5 ), // results in after=1, before=5. + 'c_between' => array( 4, 2 ), // results in after=2, before=4. + 'd_between' => array( 7 ), // won't be normalized (only 1 item). + 'f_between' => array( 10, 12 ), // not in params, skipped. + ); + $params = array( 'a', 'b', 'c', 'd' ); + $result = WC_Admin_Reports_Interval::normalize_between_params( $request, $params, true ); + $expected = array( + 'b_after' => 1, + 'b_before' => 5, + 'c_after' => 2, + 'c_before' => 4, + ); + + $this->assertEquals( $result, $expected ); + } + + /** + * Test function that validates *_between query parameters for numeric values. */ public function test_rest_validate_between_numeric_arg() { $this->assertIsWPError( @@ -851,4 +874,28 @@ class WC_Tests_Reports_Interval_Stats extends WC_Unit_Test_Case { WC_Admin_Reports_Interval::rest_validate_between_numeric_arg( array( 1, 2 ), null, 'param' ) ); } + + /** + * Test function that validates *_between query parameters for date values. + */ + public function rest_validate_between_date_arg() { + $this->assertIsWPError( + WC_Admin_Reports_Interval::rest_validate_between_date_arg( 'not array', null, 'param' ), + 'param is not a numerically indexed array.' + ); + + $this->assertIsWPError( + WC_Admin_Reports_Interval::rest_validate_between_date_arg( array( '2019-01-01T00:00:00' ), null, 'param' ), + 'param must contain 2 valid dates.' + ); + + $this->assertIsWPError( + WC_Admin_Reports_Interval::rest_validate_between_date_arg( array( 'not a valid date' ), null, 'param' ), + 'param must contain 2 valid dates.' + ); + + $this->assertTrue( + WC_Admin_Reports_Interval::rest_validate_between_date_arg( array( '2019-01-01T00:00:00', '2019-01-15T00:00:00' ), null, 'param' ) + ); + } } From 370a747c2b7cc9e4ece0923a7875f27b58005de6 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 11:05:27 +1300 Subject: [PATCH 53/91] add registered --- .../class-wc-admin-rest-reports-customers-controller.php | 7 ++++++- ...ss-wc-admin-rest-reports-customers-stats-controller.php | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index c46e8c84afd..99c093f587f 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -62,7 +62,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_after'] = $request['last_order_after']; $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $date_between_params = array( 'last_active' ); + $date_between_params = array( 'last_active', 'registered' ); $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $numeric_between_params, false ); $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $date_between_params, true ); $args = array_merge( $args, $normalized, $normalized_dates ); @@ -382,6 +382,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); + $params['registered_between'] = array( + 'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ), + ); $params['orders_count_min'] = array( 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', 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 c05b5d237c9..fadb5b8736f 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 @@ -57,7 +57,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_after'] = $request['last_order_after']; $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $date_between_params = array( 'last_active' ); + $date_between_params = array( 'last_active', 'registered' ); $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $numeric_between_params, false ); $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $date_between_params, true ); $args = array_merge( $args, $normalized, $normalized_dates ); @@ -295,6 +295,11 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); + $params['registered_between'] = array( + 'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ), + 'type' => 'array', + 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ), + ); $params['orders_count_min'] = array( 'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'wc-admin' ), 'type' => 'integer', From 41bec03880e9456fdfc55a76583c33abdc2ed692 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 11:28:58 +1300 Subject: [PATCH 54/91] tweeks --- .../class-wc-admin-rest-reports-customers-controller.php | 8 ++++---- ...s-wc-admin-rest-reports-customers-stats-controller.php | 8 ++++---- .../includes/class-wc-admin-reports-interval.php | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 99c093f587f..33c67a1fd39 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -61,10 +61,10 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $date_between_params = array( 'last_active', 'registered' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $numeric_between_params, false ); - $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $date_between_params, true ); + $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $between_params_date = array( 'last_active', 'registered' ); + $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); + $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); $args = array_merge( $args, $normalized, $normalized_dates ); return $args; 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 fadb5b8736f..aa9a7f163ca 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 @@ -56,10 +56,10 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $numeric_between_params = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $date_between_params = array( 'last_active', 'registered' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $numeric_between_params, false ); - $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $date_between_params, true ); + $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $between_params_date = array( 'last_active', 'registered' ); + $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); + $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); $args = array_merge( $args, $normalized, $normalized_dates ); return $args; 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 5036d2fcab4..2bc1465c147 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-interval.php @@ -494,7 +494,8 @@ class WC_Admin_Reports_Interval { } /** - * Normalize "*_between" parameters to "*_min" and "*_max". + * Normalize "*_between" parameters to "*_min" and "*_max" for numeric values + * and "*_after" and "*_before" for date values. * * @param array $request Query params from REST API request. * @param string|array $param_names One or more param names to handle. Should not include "_between" suffix. From e6b42631f38faa524a62990fbe2493588d54c62b Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 11:48:40 +1300 Subject: [PATCH 55/91] formatting --- ...ss-wc-admin-rest-reports-customers-controller.php | 12 ++++++------ ...admin-rest-reports-customers-stats-controller.php | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 33c67a1fd39..800e44b0e1f 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -61,11 +61,11 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $between_params_date = array( 'last_active', 'registered' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); - $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); - $args = array_merge( $args, $normalized, $normalized_dates ); + $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $normalized_numeric_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); + $between_params_date = array( 'last_active', 'registered' ); + $normalized_date_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); + $args = array_merge( $args, $normalized_numeric_params, $normalized_date_params ); return $args; } @@ -382,7 +382,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['registered_between'] = array( + $params['registered_between'] = array( 'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'array', 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ), 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 aa9a7f163ca..3265e26f2ad 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 @@ -56,11 +56,11 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_before'] = $request['last_order_before']; $args['last_order_after'] = $request['last_order_after']; - $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $between_params_date = array( 'last_active', 'registered' ); - $normalized = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); - $normalized_dates = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); - $args = array_merge( $args, $normalized, $normalized_dates ); + $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); + $normalized_numeric_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); + $between_params_date = array( 'last_active', 'registered' ); + $normalized_date_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); + $args = array_merge( $args, $normalized_numeric_params, $normalized_date_params ); return $args; } @@ -295,7 +295,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C 'format' => 'date-time', 'validate_callback' => 'rest_validate_request_arg', ); - $params['registered_between'] = array( + $params['registered_between'] = array( 'description' => __( 'Limit response to objects last active between two given ISO8601 compliant datetime.', 'wc-admin' ), 'type' => 'array', 'validate_callback' => array( 'WC_Admin_Reports_Interval', 'rest_validate_between_date_arg' ), From 765a0074918f803f2b87c302b9b85e06d1f36f52 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 12:08:44 +1300 Subject: [PATCH 56/91] tweeek --- .../class-wc-admin-rest-reports-customers-controller.php | 6 +++--- ...ass-wc-admin-rest-reports-customers-stats-controller.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php index 800e44b0e1f..f599cbe5455 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-controller.php @@ -62,10 +62,10 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control $args['last_order_after'] = $request['last_order_after']; $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $normalized_numeric_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); + $normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); $between_params_date = array( 'last_active', 'registered' ); - $normalized_date_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); - $args = array_merge( $args, $normalized_numeric_params, $normalized_date_params ); + $normalized_params_date = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); + $args = array_merge( $args, $normalized_params_numeric, $normalized_params_date ); return $args; } 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 3265e26f2ad..d500b888ff7 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 @@ -57,10 +57,10 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C $args['last_order_after'] = $request['last_order_after']; $between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' ); - $normalized_numeric_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); + $normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false ); $between_params_date = array( 'last_active', 'registered' ); - $normalized_date_params = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); - $args = array_merge( $args, $normalized_numeric_params, $normalized_date_params ); + $normalized_params_date = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_date, true ); + $args = array_merge( $args, $normalized_params_numeric, $normalized_params_date ); return $args; } From 3379491c91f99922b70075328745da0db68cc09f Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 29 Jan 2019 13:35:27 +1300 Subject: [PATCH 57/91] Customers Report: fix order count filter --- .../client/analytics/report/customers/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/analytics/report/customers/config.js b/plugins/woocommerce-admin/client/analytics/report/customers/config.js index 7869a76fe89..18a38e323ad 100644 --- a/plugins/woocommerce-admin/client/analytics/report/customers/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/customers/config.js @@ -163,7 +163,7 @@ export const advancedFilters = { } ) ), }, }, - order_count: { + orders_count: { labels: { add: __( 'No. of Orders', 'wc-admin' ), remove: __( 'Remove order filter', 'wc-admin' ), From c2eaf6d71cf4b707fac1521ba0973020306b1d65 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 29 Jan 2019 12:13:52 +0100 Subject: [PATCH 58/91] Replaced REST exception with a custom exception and handle it in REST controllers. --- ...dmin-rest-reports-coupons-stats-controller.php | 6 +++++- ...admin-rest-reports-orders-stats-controller.php | 6 +++++- ...min-rest-reports-products-stats-controller.php | 8 ++++++-- ...dmin-rest-reports-revenue-stats-controller.php | 6 +++++- .../includes/class-wc-admin-api-init.php | 3 +++ ...-wc-admin-reports-coupons-stats-segmenting.php | 4 ++-- ...s-wc-admin-reports-orders-stats-segmenting.php | 4 ++-- ...class-wc-admin-reports-parameter-exception.php | 15 +++++++++++++++ ...wc-admin-reports-products-stats-segmenting.php | 4 ++-- 9 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-parameter-exception.php 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 07a2fc1a15e..6cc198633db 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 @@ -62,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 ), 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 87895d06e45..549badede97 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 @@ -70,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 ), 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 d9c4073dbec..a5436bb30d4 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 ), 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 af71b1dd9f2..15749a93342 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 @@ -60,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 ), 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 fd51232e7f3..446cea15b7a 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,9 @@ 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'; + // Segmentation. require_once dirname( __FILE__ ) . '/class-wc-admin-reports-segmenting.php'; require_once dirname( __FILE__ ) . '/class-wc-admin-reports-orders-stats-segmenting.php'; 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 index cbb898e9534..dcb82d4040e 100644 --- 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 @@ -244,7 +244,7 @@ class WC_Admin_Reports_Coupons_Stats_Segmenting extends WC_Admin_Reports_Segment * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). * * @return array - * @throws WC_REST_Exception In case of segmenting by variations, when no parent product is specified. + * @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; @@ -271,7 +271,7 @@ class WC_Admin_Reports_Coupons_Stats_Segmenting extends WC_Admin_Reports_Segment $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_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + 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( 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 index 8d244feea52..58cd3ae6bc6 100644 --- 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 @@ -335,7 +335,7 @@ class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenti * @param string $table_name Name of main SQL table for the data store (used as basis for JOINS). * * @return array - * @throws WC_REST_Exception In case of segmenting by variations, when no parent product is specified. + * @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; @@ -363,7 +363,7 @@ class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenti $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_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + 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( 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 @@ +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_REST_Exception( 'woocommerce_rest_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'wc-admin' ), 400 ); + 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( From bee26375f9326ee09d6b7c565aa6a90b1c316544 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 29 Jan 2019 12:18:30 +0100 Subject: [PATCH 59/91] Validate limit clause and set empty default if there is no limit. --- .../class-wc-admin-reports-orders-stats-segmenting.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 index 58cd3ae6bc6..ee4d49789a4 100644 --- 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 @@ -297,9 +297,12 @@ class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenti */ 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'] ); - $orig_rowcount = intval( $limit_parts[1] ); - $segmenting_limit = $limit_parts[0] . ',' . $orig_rowcount * count( $this->get_all_segments() ); + 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 From a3ae15d58e0398f9342c90fc79bfe255f8ffbb7a Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 29 Jan 2019 12:52:20 +0100 Subject: [PATCH 60/91] Removed obsolete comment. --- .../class-wc-admin-rest-reports-coupons-stats-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6cc198633db..b99e526eb32 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 @@ -343,7 +343,7 @@ class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Con 'enum' => array( 'product', 'variation', - 'category', // will we support this? + 'category', 'coupon', ), 'validate_callback' => 'rest_validate_request_arg', From e56009cfdc1afb39a8eb0935b4bc3db3168e3c51 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 29 Jan 2019 13:29:50 +0100 Subject: [PATCH 61/91] Updated todo format to be in line with the rest of WooCommerce. --- .../api/class-wc-admin-rest-customers-controller.php | 2 +- ...admin-rest-reports-customers-stats-controller.php | 6 +++--- .../includes/class-wc-admin-api-init.php | 6 +++--- .../class-wc-admin-reports-customers-stats-query.php | 2 +- .../includes/class-wc-admin-reports-interval.php | 2 +- ...lass-wc-admin-reports-orders-stats-segmenting.php | 4 ++-- .../includes/class-wc-admin-reports-segmenting.php | 4 ++-- .../class-wc-admin-reports-categories-data-store.php | 2 +- .../class-wc-admin-reports-customers-data-store.php | 2 +- .../class-wc-admin-reports-data-store.php | 12 ++++++------ ...lass-wc-admin-reports-orders-stats-data-store.php | 6 +++--- .../class-wc-admin-reports-products-data-store.php | 8 ++++---- 12 files changed, 28 insertions(+), 28 deletions(-) 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-customers-stats-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-reports-customers-stats-controller.php index af24ea6119d..ef288330a47 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 @@ -75,7 +75,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() ), ); @@ -117,7 +117,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' ), @@ -159,7 +159,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/class-wc-admin-api-init.php b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php index 446cea15b7a..c47a38bc03a 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -463,7 +463,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 ); @@ -614,7 +614,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 ); } } @@ -661,7 +661,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", 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 27e576e6065..55bb4c49120 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 index ee4d49789a4..c6757919802 100644 --- 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 @@ -27,7 +27,7 @@ class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenti '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. + // @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", ); @@ -354,7 +354,7 @@ class WC_Admin_Reports_Orders_Stats_Segmenting extends WC_Admin_Reports_Segmenti // 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? + // @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 ), diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php index 666a31d49b3..29d38d331c8 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php @@ -292,7 +292,7 @@ class WC_Admin_Reports_Segmenting { ) ); } elseif ( 'variation' === $this->query_args['segmentby'] ) { - // TODO: assuming that this will only be used for one product, check assumption. + // @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; @@ -314,7 +314,7 @@ class WC_Admin_Reports_Segmenting { ); $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: switch to a non-direct-SQL way to get all coupons? $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'] ) { 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-customers-data-store.php b/plugins/woocommerce-admin/includes/data-stores/class-wc-admin-reports-customers-data-store.php index 17e88cba209..277aa2a9bf0 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 3630b8e6b74..19674ce626e 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; @@ -121,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' ) ); @@ -132,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 ); @@ -336,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 ); } @@ -460,7 +460,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 b6b68a91262..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 @@ -74,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 ); @@ -89,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 = ''; @@ -98,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 ); 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 846f88751c6..4f11c458e77 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 @@ -353,7 +353,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i // Shipping amount tax based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php // distribute simply based on number of line items. $shipping_tax_amount = 0; - // TODO: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? + // @todo: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? $order_taxes = $order->get_taxes(); $line_items_shipping = $order->get_items( 'shipping' ); $total_shipping_tax_amount = 0; @@ -375,7 +375,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i $shipping_tax_amount = $total_shipping_tax_amount / $order_items * $product_qty; // Tax amount. - // TODO: check if this calculates tax correctly with refunds. + // @todo: check if this calculates tax correctly with refunds. $tax_amount = 0; $order_taxes = $order->get_taxes(); @@ -385,7 +385,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i $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? + // @todo: should net revenue be affected by refunds, as refunds are tracked separately? $net_revenue = $order_item->get_subtotal( 'edit' ) - $amount_refunded; // Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php. @@ -413,7 +413,7 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i 'tax_amount' => $tax_amount, 'shipping_amount' => $shipping_amount, 'shipping_tax_amount' => $shipping_tax_amount, - // TODO: can this be incorrect if modified by filters? + // @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, ), From 6efef57cbad1fa26f07d423b5caedb29280b7e7e Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Tue, 29 Jan 2019 19:57:04 +0100 Subject: [PATCH 62/91] Factored out order-related product-level calculations to WC_Admin_Order and plugged it into the WC framework. --- .../includes/class-wc-admin-api-init.php | 6 + .../includes/class-wc-admin-order.php | 189 ++++++++++++++++++ ...s-wc-admin-reports-products-data-store.php | 74 +------ 3 files changed, 202 insertions(+), 67 deletions(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-order.php 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 c47a38bc03a..25da857b9c6 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -113,6 +113,9 @@ class WC_Admin_Api_Init { // 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'; @@ -414,6 +417,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(); 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/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 4f11c458e77..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,48 +331,14 @@ 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; - $product_qty = $order_item->get_quantity( 'edit' ) - $quantity_refunded; - - // Shipping amount based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php - // distributed simply based on number of line items. - $order_items = $order->get_item_count(); - $refunded = $order->get_total_shipping_refunded(); - if ( $refunded > 0 ) { - $total_shipping_amount = $order->get_shipping_total() - $refunded; - } else { - $total_shipping_amount = $order->get_shipping_total(); - } - $shipping_amount = $total_shipping_amount / $order_items * $product_qty; - - // Shipping amount tax based on woocommerce code in includes/admin/meta-boxes/views/html-order-item(s).php - // distribute simply based on number of line items. - $shipping_tax_amount = 0; - // @todo: if WC is currently not tax enabled, but it was before (or vice versa), would this work correctly? - $order_taxes = $order->get_taxes(); - $line_items_shipping = $order->get_items( 'shipping' ); - $total_shipping_tax_amount = 0; - foreach ( $line_items_shipping as $item_id => $item ) { - $tax_data = $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 = $order->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; - } - } - } - } - $shipping_tax_amount = $total_shipping_tax_amount / $order_items * $product_qty; + $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. @@ -388,9 +354,6 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i // @todo: should net revenue be affected by refunds, as refunds are tracked separately? $net_revenue = $order_item->get_subtotal( 'edit' ) - $amount_refunded; - // Coupon calculation based on woocommerce code in includes/admin/meta-boxes/views/html-order-item.php. - $coupon_amount = $order_item->get_subtotal( 'edit' ) - $order_item->get_total( 'edit' ); - if ( $quantity_refunded >= $order_item->get_quantity( 'edit' ) ) { $wpdb->delete( $wpdb->prefix . self::TABLE_NAME, @@ -438,27 +401,4 @@ class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store i } } - /** - * 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; - } - } From dfd15f959a184b733fd7dbf99f890a3dc3b875f3 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Tue, 29 Jan 2019 16:19:00 -0500 Subject: [PATCH 63/91] Components package: Add emoji-flags dependency (https://github.com/woocommerce/woocommerce-admin/pull/1411) * Add emoji-flags to the components package.json * Update the changelog --- plugins/woocommerce-admin/packages/components/CHANGELOG.md | 5 ++++- plugins/woocommerce-admin/packages/components/package.json | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/packages/components/CHANGELOG.md b/plugins/woocommerce-admin/packages/components/CHANGELOG.md index 7234f23da80..6f38b0cf78b 100644 --- a/plugins/woocommerce-admin/packages/components/CHANGELOG.md +++ b/plugins/woocommerce-admin/packages/components/CHANGELOG.md @@ -1,4 +1,7 @@ -# 1.4.1 (unreleased) +# 1.4.2 (unreleased) +- Add emoji-flags dependency + +# 1.4.1 - Chart component: format numbers and prices using store currency settings. - Make `href`/linking optional in SummaryNumber. - Fix SummaryNumber example code. diff --git a/plugins/woocommerce-admin/packages/components/package.json b/plugins/woocommerce-admin/packages/components/package.json index c03ece5c319..3738ab1855e 100644 --- a/plugins/woocommerce-admin/packages/components/package.json +++ b/plugins/woocommerce-admin/packages/components/package.json @@ -44,6 +44,7 @@ "d3-selection": "^1.3.2", "d3-shape": "^1.2.2", "d3-time-format": "^2.1.3", + "emoji-flags": "^1.2.0", "gridicons": "3.1.1", "interpolate-components": "1.1.1", "lodash": "^4.17.11", @@ -52,8 +53,7 @@ "qs": "^6.5.2", "react-dates": "^18.0.4", "react-router-dom": "4.3.1", - "react-transition-group": "^2.4.0", - "react-world-flags": "1.2.4" + "react-transition-group": "^2.4.0" }, "publishConfig": { "access": "public" From 88ea6136274124475117db8e272c099e0f471d9f Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Tue, 29 Jan 2019 15:37:25 -0800 Subject: [PATCH 64/91] Build: Updates from latest npm publish (https://github.com/woocommerce/woocommerce-admin/pull/1409) * remove unreleased tags from changelogs * chore(release): publish - @woocommerce/components@1.4.1 - @woocommerce/csv-export@1.0.3 - @woocommerce/currency@1.1.0 - @woocommerce/date@1.0.6 - @woocommerce/navigation@1.1.1 - @woocommerce/number@1.0.1 --- .../woocommerce-admin/packages/components/package.json | 10 +++++----- .../woocommerce-admin/packages/csv-export/package.json | 2 +- .../woocommerce-admin/packages/currency/CHANGELOG.md | 2 +- .../woocommerce-admin/packages/currency/package.json | 6 +++--- plugins/woocommerce-admin/packages/date/package.json | 2 +- .../woocommerce-admin/packages/navigation/package.json | 2 +- plugins/woocommerce-admin/packages/number/package.json | 10 +++++----- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/plugins/woocommerce-admin/packages/components/package.json b/plugins/woocommerce-admin/packages/components/package.json index 3738ab1855e..46070b6560d 100644 --- a/plugins/woocommerce-admin/packages/components/package.json +++ b/plugins/woocommerce-admin/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/components", - "version": "1.4.0", + "version": "1.4.1", "description": "UI components for WooCommerce.", "author": "Automattic", "license": "GPL-2.0-or-later", @@ -22,10 +22,10 @@ "react-native": "src/index", "dependencies": { "@babel/runtime-corejs2": "7.2.0", - "@woocommerce/csv-export": "^1.0.2", - "@woocommerce/currency": "^1.0.0", - "@woocommerce/date": "^1.0.5", - "@woocommerce/navigation": "^1.1.0", + "@woocommerce/csv-export": "^1.0.3", + "@woocommerce/currency": "^1.1.0", + "@woocommerce/date": "^1.0.6", + "@woocommerce/navigation": "^1.1.1", "@wordpress/components": "7.0.5", "@wordpress/compose": "3.0.0", "@wordpress/date": "3.0.1", diff --git a/plugins/woocommerce-admin/packages/csv-export/package.json b/plugins/woocommerce-admin/packages/csv-export/package.json index 7d84f4a5a89..c917f8e0c28 100644 --- a/plugins/woocommerce-admin/packages/csv-export/package.json +++ b/plugins/woocommerce-admin/packages/csv-export/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/csv-export", - "version": "1.0.2", + "version": "1.0.3", "description": "WooCommerce utility library to convert data to CSV files.", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/plugins/woocommerce-admin/packages/currency/CHANGELOG.md b/plugins/woocommerce-admin/packages/currency/CHANGELOG.md index 7963769c489..95373e4bebd 100644 --- a/plugins/woocommerce-admin/packages/currency/CHANGELOG.md +++ b/plugins/woocommerce-admin/packages/currency/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.1.0 (unreleased) +# 1.1.0 - Format using store currency settings (instead of locale) - Add optional currency symbol parameter diff --git a/plugins/woocommerce-admin/packages/currency/package.json b/plugins/woocommerce-admin/packages/currency/package.json index d6cf273ca4f..ee203bada18 100644 --- a/plugins/woocommerce-admin/packages/currency/package.json +++ b/plugins/woocommerce-admin/packages/currency/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/currency", - "version": "1.0.0", + "version": "1.1.0", "description": "WooCommerce currency utilities.", "author": "Automattic", "license": "GPL-2.0-or-later", @@ -22,8 +22,8 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.0.0", - "lodash": "^4.17.11", - "@woocommerce/number": "1.0.0" + "@woocommerce/number": "^1.0.1", + "lodash": "^4.17.11" }, "publishConfig": { "access": "public" diff --git a/plugins/woocommerce-admin/packages/date/package.json b/plugins/woocommerce-admin/packages/date/package.json index 4b4c8a7f90e..1c8f3637964 100644 --- a/plugins/woocommerce-admin/packages/date/package.json +++ b/plugins/woocommerce-admin/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/date", - "version": "1.0.5", + "version": "1.0.6", "description": "WooCommerce date utilities.", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/plugins/woocommerce-admin/packages/navigation/package.json b/plugins/woocommerce-admin/packages/navigation/package.json index c641c72cd42..74b17e1b7f9 100644 --- a/plugins/woocommerce-admin/packages/navigation/package.json +++ b/plugins/woocommerce-admin/packages/navigation/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/navigation", - "version": "1.1.0", + "version": "1.1.1", "description": "WooCommerce navigation utilities.", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/plugins/woocommerce-admin/packages/number/package.json b/plugins/woocommerce-admin/packages/number/package.json index c8a920cc12c..e9bb5388c32 100644 --- a/plugins/woocommerce-admin/packages/number/package.json +++ b/plugins/woocommerce-admin/packages/number/package.json @@ -1,6 +1,6 @@ { "name": "@woocommerce/number", - "version": "1.0.0", + "version": "1.0.1", "description": "Number formatting utilities for WooCommerce.", "author": "Automattic", "license": "GPL-2.0-or-later", @@ -20,11 +20,11 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime-corejs2": "7.1.5", - "locutus": "^2.0.10", - "lodash": "^4.17.11" + "@babel/runtime-corejs2": "7.1.5", + "locutus": "^2.0.10", + "lodash": "^4.17.11" }, "publishConfig": { "access": "public" } -} \ No newline at end of file +} From 53a6e498740a8807efc77ad33520bf865633599f Mon Sep 17 00:00:00 2001 From: Justin Shreve Date: Wed, 30 Jan 2019 02:22:10 -0500 Subject: [PATCH 65/91] Finish cleaning up client/store. (https://github.com/woocommerce/woocommerce-admin/pull/1403) --- .../analytics/components/leaderboard/index.js | 2 +- .../components/leaderboard/test/index.js | 2 +- .../analytics/components/report-chart/index.js | 2 +- .../analytics/components/report-summary/index.js | 2 +- .../analytics/components/report-table/index.js | 2 +- .../client/analytics/report/customers/config.js | 2 +- .../client/analytics/report/revenue/table.js | 2 +- .../client/analytics/report/taxes/config.js | 2 +- .../client/header/activity-panel/panels/inbox.js | 2 +- .../header/activity-panel/panels/orders.js | 2 +- .../header/activity-panel/panels/reviews.js | 2 +- .../client/lib/async-requests/index.js | 2 +- .../woocommerce-admin/client/store/constants.js | 16 ---------------- plugins/woocommerce-admin/client/store/utils.js | 11 ----------- .../woocommerce-admin/client/wc-api/constants.js | 3 +++ .../client/wc-api/reports/stats/operations.js | 5 ++--- .../client/{store => wc-api}/reports/utils.js | 2 +- 17 files changed, 18 insertions(+), 43 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/store/constants.js delete mode 100644 plugins/woocommerce-admin/client/store/utils.js rename plugins/woocommerce-admin/client/{store => wc-api}/reports/utils.js (99%) diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js index 25989a874fa..41723e20f23 100644 --- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js @@ -17,7 +17,7 @@ import { Card, EmptyTable, TableCard } from '@woocommerce/components'; * Internal dependencies */ import ReportError from 'analytics/components/report-error'; -import { getReportTableData } from 'store/reports/utils'; +import { getReportTableData } from 'wc-api/reports/utils'; import withSelect from 'wc-api/with-select'; import './style.scss'; diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js index 368e8ff9d48..1cfc9e5c699 100644 --- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/test/index.js @@ -18,7 +18,7 @@ import { numberFormat } from '@woocommerce/number'; * Internal dependencies */ import LeaderboardWithSelect, { Leaderboard } from '../'; -import { NAMESPACE } from 'store/constants'; +import { NAMESPACE } from 'wc-api/constants'; import mockData from '../__mocks__/top-selling-products-mock-data'; // Mock to avoid tests failing due to it using DOM properties that diff --git a/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js b/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js index 283beabbb40..58917aed6a2 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-chart/index.js @@ -25,7 +25,7 @@ import { Chart } from '@woocommerce/components'; /** * Internal dependencies */ -import { getReportChartData, getTooltipValueFormat } from 'store/reports/utils'; +import { getReportChartData, getTooltipValueFormat } from 'wc-api/reports/utils'; import ReportError from 'analytics/components/report-error'; import withSelect from 'wc-api/with-select'; diff --git a/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js b/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js index 89e2a2f309f..51a6ea4f1e2 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-summary/index.js @@ -19,7 +19,7 @@ import { formatCurrency } from '@woocommerce/currency'; /** * Internal dependencies */ -import { getSummaryNumbers } from 'store/reports/utils'; +import { getSummaryNumbers } from 'wc-api/reports/utils'; import ReportError from 'analytics/components/report-error'; import withSelect from 'wc-api/with-select'; diff --git a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js index 4e922dde521..55b664bebf7 100644 --- a/plugins/woocommerce-admin/client/analytics/components/report-table/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/report-table/index.js @@ -19,7 +19,7 @@ import { onQueryChange } from '@woocommerce/navigation'; * Internal dependencies */ import ReportError from 'analytics/components/report-error'; -import { getReportChartData, getReportTableData } from 'store/reports/utils'; +import { getReportChartData, getReportTableData } from 'wc-api/reports/utils'; import withSelect from 'wc-api/with-select'; import { extendTableData } from './utils'; diff --git a/plugins/woocommerce-admin/client/analytics/report/customers/config.js b/plugins/woocommerce-admin/client/analytics/report/customers/config.js index 18a38e323ad..bdcd9827930 100644 --- a/plugins/woocommerce-admin/client/analytics/report/customers/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/customers/config.js @@ -9,7 +9,7 @@ import { decodeEntities } from '@wordpress/html-entities'; * Internal dependencies */ import { getCustomerLabels, getRequestByIdString } from 'lib/async-requests'; -import { NAMESPACE } from 'store/constants'; +import { NAMESPACE } from 'wc-api/constants'; export const filters = [ { diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/table.js b/plugins/woocommerce-admin/client/analytics/report/revenue/table.js index aa0e7c23f14..7a2230fb4e4 100644 --- a/plugins/woocommerce-admin/client/analytics/report/revenue/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/revenue/table.js @@ -19,7 +19,7 @@ import { numberFormat } from '@woocommerce/number'; /** * Internal dependencies */ -import { QUERY_DEFAULTS } from 'store/constants'; +import { QUERY_DEFAULTS } from 'wc-api/constants'; import ReportTable from 'analytics/components/report-table'; import withSelect from 'wc-api/with-select'; diff --git a/plugins/woocommerce-admin/client/analytics/report/taxes/config.js b/plugins/woocommerce-admin/client/analytics/report/taxes/config.js index 39b236447b9..4eaf222afae 100644 --- a/plugins/woocommerce-admin/client/analytics/report/taxes/config.js +++ b/plugins/woocommerce-admin/client/analytics/report/taxes/config.js @@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n'; */ import { getRequestByIdString } from 'lib/async-requests'; import { getTaxCode } from './utils'; -import { NAMESPACE } from 'store/constants'; +import { NAMESPACE } from 'wc-api/constants'; export const charts = [ { diff --git a/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js index 31ba3e18a80..287b8df3f00 100644 --- a/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js +++ b/plugins/woocommerce-admin/client/header/activity-panel/panels/inbox.js @@ -16,7 +16,7 @@ import { ActivityCard, ActivityCardPlaceholder } from '../activity-card'; import ActivityHeader from '../activity-header'; import { EmptyContent, Section } from '@woocommerce/components'; import sanitizeHTML from 'lib/sanitize-html'; -import { QUERY_DEFAULTS } from 'store/constants'; +import { QUERY_DEFAULTS } from 'wc-api/constants'; class InboxPanel extends Component { render() { diff --git a/plugins/woocommerce-admin/client/header/activity-panel/panels/orders.js b/plugins/woocommerce-admin/client/header/activity-panel/panels/orders.js index c1c4b2b4b62..8c0412a70ff 100644 --- a/plugins/woocommerce-admin/client/header/activity-panel/panels/orders.js +++ b/plugins/woocommerce-admin/client/header/activity-panel/panels/orders.js @@ -33,7 +33,7 @@ import { ActivityCard, ActivityCardPlaceholder } from '../activity-card'; import ActivityHeader from '../activity-header'; import ActivityOutboundLink from '../activity-outbound-link'; import { getOrderRefundTotal } from 'lib/order-values'; -import { QUERY_DEFAULTS } from 'store/constants'; +import { QUERY_DEFAULTS } from 'wc-api/constants'; import withSelect from 'wc-api/with-select'; function OrdersPanel( { orders, isRequesting, isError } ) { diff --git a/plugins/woocommerce-admin/client/header/activity-panel/panels/reviews.js b/plugins/woocommerce-admin/client/header/activity-panel/panels/reviews.js index 7389e33e29f..0db42c951e5 100644 --- a/plugins/woocommerce-admin/client/header/activity-panel/panels/reviews.js +++ b/plugins/woocommerce-admin/client/header/activity-panel/panels/reviews.js @@ -28,7 +28,7 @@ import { */ import { ActivityCard, ActivityCardPlaceholder } from '../activity-card'; import ActivityHeader from '../activity-header'; -import { QUERY_DEFAULTS } from 'store/constants'; +import { QUERY_DEFAULTS } from 'wc-api/constants'; import sanitizeHTML from 'lib/sanitize-html'; import withSelect from 'wc-api/with-select'; diff --git a/plugins/woocommerce-admin/client/lib/async-requests/index.js b/plugins/woocommerce-admin/client/lib/async-requests/index.js index 43d42b7aaab..1e4597ad8e4 100644 --- a/plugins/woocommerce-admin/client/lib/async-requests/index.js +++ b/plugins/woocommerce-admin/client/lib/async-requests/index.js @@ -13,7 +13,7 @@ import { getIdsFromQuery, stringifyQuery } from '@woocommerce/navigation'; /** * Internal dependencies */ -import { NAMESPACE } from 'store/constants'; +import { NAMESPACE } from 'wc-api/constants'; /** * Get a function that accepts ids as they are found in url parameter and diff --git a/plugins/woocommerce-admin/client/store/constants.js b/plugins/woocommerce-admin/client/store/constants.js deleted file mode 100644 index 6e801e08857..00000000000 --- a/plugins/woocommerce-admin/client/store/constants.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @format - */ - -export const NAMESPACE = '/wc/v4/'; -export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/'; -export const ERROR = 'ERROR'; - -// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter -export const MAX_PER_PAGE = 100; - -export const QUERY_DEFAULTS = { - pageSize: 25, - period: 'month', - compare: 'previous_year', -}; diff --git a/plugins/woocommerce-admin/client/store/utils.js b/plugins/woocommerce-admin/client/store/utils.js deleted file mode 100644 index b2830ab87a9..00000000000 --- a/plugins/woocommerce-admin/client/store/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @format */ - -/** - * Returns a string representation of a sorted query object. - * - * @param {Object} query Current state - * @return {String} Query Key - */ -export function getJsonString( query = {} ) { - return JSON.stringify( query, Object.keys( query ).sort() ); -} diff --git a/plugins/woocommerce-admin/client/wc-api/constants.js b/plugins/woocommerce-admin/client/wc-api/constants.js index 94b585332fc..1cdeeadfd86 100644 --- a/plugins/woocommerce-admin/client/wc-api/constants.js +++ b/plugins/woocommerce-admin/client/wc-api/constants.js @@ -6,6 +6,9 @@ import { MINUTE } from '@fresh-data/framework'; export const NAMESPACE = '/wc/v4'; +// TODO: Remove once swagger endpoints are phased out. +export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/wc-v3-api/1.0.0/'; + export const DEFAULT_REQUIREMENT = { timeout: 1 * MINUTE, freshness: 5 * MINUTE, diff --git a/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js b/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js index 97193986cd9..5321ea838df 100644 --- a/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js +++ b/plugins/woocommerce-admin/client/wc-api/reports/stats/operations.js @@ -12,9 +12,8 @@ import { stringifyQuery } from '@woocommerce/navigation'; /** * Internal dependencies */ -import { getResourceIdentifier, getResourcePrefix } from '../../utils'; -import { NAMESPACE } from '../../constants'; -import { SWAGGERNAMESPACE } from 'store/constants'; +import { getResourceIdentifier, getResourcePrefix } from 'wc-api/utils'; +import { NAMESPACE, SWAGGERNAMESPACE } from 'wc-api/constants'; const statEndpoints = [ 'coupons', diff --git a/plugins/woocommerce-admin/client/store/reports/utils.js b/plugins/woocommerce-admin/client/wc-api/reports/utils.js similarity index 99% rename from plugins/woocommerce-admin/client/store/reports/utils.js rename to plugins/woocommerce-admin/client/wc-api/reports/utils.js index 1ab57f1845d..f008a24dd95 100644 --- a/plugins/woocommerce-admin/client/store/reports/utils.js +++ b/plugins/woocommerce-admin/client/wc-api/reports/utils.js @@ -16,7 +16,7 @@ import { formatCurrency } from '@woocommerce/currency'; /** * Internal dependencies */ -import { MAX_PER_PAGE, QUERY_DEFAULTS } from 'store/constants'; +import { MAX_PER_PAGE, QUERY_DEFAULTS } from 'wc-api/constants'; import * as categoriesConfig from 'analytics/report/categories/config'; import * as couponsConfig from 'analytics/report/coupons/config'; import * as customersConfig from 'analytics/report/customers/config'; From 08dfea7cff849f283018af07a1c20b31d11a8d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Wed, 30 Jan 2019 10:43:30 +0100 Subject: [PATCH 66/91] Add Taxes REST controller override (https://github.com/woocommerce/woocommerce-admin/pull/1407) --- .../class-wc-admin-rest-taxes-controller.php | 27 +++++++++++++++++++ .../includes/class-wc-admin-api-init.php | 13 +++++++++ 2 files changed, 40 insertions(+) create mode 100644 plugins/woocommerce-admin/includes/api/class-wc-admin-rest-taxes-controller.php diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-taxes-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-taxes-controller.php new file mode 100644 index 00000000000..d30b678b7d3 --- /dev/null +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-taxes-controller.php @@ -0,0 +1,27 @@ + Date: Wed, 30 Jan 2019 12:16:37 +0100 Subject: [PATCH 67/91] Added segmentby to schema, query arguments and allows parameters in the controller for taxes/stats. --- ...in-rest-reports-taxes-stats-controller.php | 81 ++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) 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..800d9fadd95 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,36 @@ 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' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'enum' => array( 'day', 'week', 'month', 'year' ), + ), + '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 +304,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 +314,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 +323,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 +355,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 +369,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 +378,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_code', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } From 0ddc6d5e41619e35b403283d62887e6c99c276e6 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 30 Jan 2019 18:36:20 +0100 Subject: [PATCH 68/91] Added segmenting code for taxes/stats. --- ...in-rest-reports-taxes-stats-controller.php | 2 +- .../includes/class-wc-admin-api-init.php | 1 + .../class-wc-admin-reports-segmenting.php | 11 ++ ...c-admin-reports-taxes-stats-segmenting.php | 147 ++++++++++++++++++ ...c-admin-reports-taxes-stats-data-store.php | 3 + 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce-admin/includes/class-wc-admin-reports-taxes-stats-segmenting.php 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 800d9fadd95..290b8712df0 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 @@ -382,7 +382,7 @@ class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Contr 'description' => __( 'Segment the response by additional constraint.', 'wc-admin' ), 'type' => 'string', 'enum' => array( - 'tax_code', + 'tax_rate_id', ), 'validate_callback' => 'rest_validate_request_arg', ); 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 25da857b9c6..d324ed756fb 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-api-init.php @@ -121,6 +121,7 @@ class WC_Admin_Api_Init { 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'; diff --git a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php index 29d38d331c8..d111d8044a3 100644 --- a/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php +++ b/plugins/woocommerce-admin/includes/class-wc-admin-reports-segmenting.php @@ -315,12 +315,23 @@ class WC_Admin_Reports_Segmenting { $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(); 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-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 ); From e433609d9dd1ea47bb113d0f913f03a205da9594 Mon Sep 17 00:00:00 2001 From: Peter Fabian Date: Wed, 30 Jan 2019 18:46:06 +0100 Subject: [PATCH 69/91] Updated test of schema for taxes/stats. --- .../tests/api/reports-taxes-stats.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 ); } } From d4a7cec89a476e8c47edb8fdbe91de44ed56bc89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 13:10:27 -0500 Subject: [PATCH 70/91] Update dependency eslint-plugin-jest to v22.2.1 (https://github.com/woocommerce/woocommerce-admin/pull/1415) --- plugins/woocommerce-admin/package-lock.json | 47 +++++++++++++++------ plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index d648bb3d958..070bc26ae1f 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -7842,9 +7842,9 @@ } }, "eslint-plugin-jest": { - "version": "22.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.1.3.tgz", - "integrity": "sha512-JTZTI6WQoNruAugNyCO8fXfTONVcDd5i6dMRFA5g3rUFn1UDDLILY1bTL6alvNXbW2U7Sc2OSpi8m08pInnq0A==", + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.2.1.tgz", + "integrity": "sha512-cpxjB7xvPAEZYed5atKGD94TOYgV73qaX4XQ+E6DH0Y/xADcgqAYZKyZsFZ+qs/ll9wnNVZJdxZxav2vWYXOqw==", "dev": true }, "eslint-plugin-jsx-a11y": { @@ -8978,7 +8978,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -8996,11 +8997,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9013,15 +9016,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9124,7 +9130,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -9134,6 +9141,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9146,17 +9154,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -9173,6 +9184,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -9245,7 +9257,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9255,6 +9268,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -9330,7 +9344,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -9360,6 +9375,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9377,6 +9393,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9415,11 +9432,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index b969ea7aa6a..cb3feee6c3f 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -76,7 +76,7 @@ "eslint": "5.12.0", "eslint-config-wpcalypso": "4.0.1", "eslint-loader": "2.1.1", - "eslint-plugin-jest": "22.1.3", + "eslint-plugin-jest": "22.2.1", "eslint-plugin-jsx-a11y": "6.1.2", "eslint-plugin-react": "7.12.3", "eslint-plugin-wpcalypso": "4.0.2", From e2da828d1f741830df106c7901457c1b15300d25 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 13:14:58 -0500 Subject: [PATCH 71/91] Update dependency lerna to v3.10.7 (https://github.com/woocommerce/woocommerce-admin/pull/1419) --- plugins/woocommerce-admin/package-lock.json | 274 ++++++++++---------- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 138 insertions(+), 138 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 070bc26ae1f..d89aa78bcdf 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -738,14 +738,14 @@ } }, "@lerna/add": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.10.5.tgz", - "integrity": "sha512-T3d9FnSyBOYnM/a1j5Sa65SGOTgnv04HG7Y2lRWJcF6PvOoTsozYW0izi/vLAnAt/DvGhYf2morXkWS8AbIeDg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.10.6.tgz", + "integrity": "sha512-FxQ5Bmyb5fF+3BQiNffM6cTeGCrl4uaAuGvxFIWF6Pgz6U14tUc1e16xgKDvVb1CurzJgIV5sLOT5xmCOqv1kA==", "dev": true, "requires": { - "@lerna/bootstrap": "3.10.5", - "@lerna/command": "3.10.0", - "@lerna/filter-options": "3.10.1", + "@lerna/bootstrap": "3.10.6", + "@lerna/command": "3.10.6", + "@lerna/filter-options": "3.10.6", "@lerna/npm-conf": "3.7.0", "@lerna/validation-error": "3.6.0", "dedent": "^0.7.0", @@ -755,28 +755,28 @@ } }, "@lerna/batch-packages": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.10.0.tgz", - "integrity": "sha512-ERvnpmmfV8H+3B+9FmHqmzfgz0xVe3ktW/e4WZZXYMGpqSGToILZlai4PsBrW5gUtnXA77LSskME+aRdkZaKsQ==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.10.6.tgz", + "integrity": "sha512-sInr3ZQJFMh9Zq+ZUoVjX8R67j9ViRkVy0uEMsOfG+jZlXj1lRPRMPRiRgU0jXSYEwCdwuAB5pTd9tTx0VCJUw==", "dev": true, "requires": { - "@lerna/package-graph": "3.10.0", + "@lerna/package-graph": "3.10.6", "@lerna/validation-error": "3.6.0", "libnpm": "^2.0.1" } }, "@lerna/bootstrap": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.10.5.tgz", - "integrity": "sha512-WMUfysmX2WFkOzWcpv0mW6Kw91Zsuq9Ecz/TIT4q3FywvABD0mrWbcDXSyrxMspxDEOtPqM/Lk9nm6F9M98kbg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.10.6.tgz", + "integrity": "sha512-qbGjAxRpV/eiI9CboUIpsPPGpSogs8mN2/iDaAUBTaWVFVz/YyU64nui84Gll0kbdaHOyPput+kk2S8NCSCCdg==", "dev": true, "requires": { - "@lerna/batch-packages": "3.10.0", - "@lerna/command": "3.10.0", - "@lerna/filter-options": "3.10.1", + "@lerna/batch-packages": "3.10.6", + "@lerna/command": "3.10.6", + "@lerna/filter-options": "3.10.6", "@lerna/has-npm-version": "3.10.0", "@lerna/npm-install": "3.10.0", - "@lerna/package-graph": "3.10.0", + "@lerna/package-graph": "3.10.6", "@lerna/pulse-till-done": "3.7.1", "@lerna/rimraf-dir": "3.10.0", "@lerna/run-lifecycle": "3.10.5", @@ -797,16 +797,16 @@ } }, "@lerna/changed": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.10.5.tgz", - "integrity": "sha512-Uy3VWzjmGg2CjKRTW9os+R6Itg3LVJ6CjczeOsOFwSN4JMdNoObUnCTSdCCTUF/+hQNAoSnkw3+C8dC5FPL1Zw==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.10.6.tgz", + "integrity": "sha512-nZDVq/sKdhgoAg1BVnpqjqUUz5+zedG+AnU+6mjEN2f23YVtRCsW55N4I9eEdW2pxXUaCY85Hj/HPSA74BYaFg==", "dev": true, "requires": { "@lerna/collect-updates": "3.10.1", - "@lerna/command": "3.10.0", - "@lerna/listable": "3.10.0", + "@lerna/command": "3.10.6", + "@lerna/listable": "3.10.6", "@lerna/output": "3.6.0", - "@lerna/version": "3.10.5" + "@lerna/version": "3.10.6" } }, "@lerna/check-working-tree": { @@ -870,13 +870,13 @@ } }, "@lerna/clean": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.10.1.tgz", - "integrity": "sha512-eYSNSD4xD//OIDe0r4r/HhEMEXriIuKqp4BMDhrO7pJmYhk7FvznJUSc4jc85wdA4Y0ooqSs9gF/w2lgLGgUxw==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.10.6.tgz", + "integrity": "sha512-MuL8HOwnyvVtr6GOiAN/Ofjbx+BJdCrtjrM1Uuh8FFnbnZTPVf+0MPxL2jVzPMo0PmoIrX3fvlwvzKNk/lH0Ug==", "dev": true, "requires": { - "@lerna/command": "3.10.0", - "@lerna/filter-options": "3.10.1", + "@lerna/command": "3.10.6", + "@lerna/filter-options": "3.10.6", "@lerna/prompt": "3.6.0", "@lerna/pulse-till-done": "3.7.1", "@lerna/rimraf-dir": "3.10.0", @@ -886,12 +886,12 @@ } }, "@lerna/cli": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.10.0.tgz", - "integrity": "sha512-OTO8GlD6Rf298hxml3/Y3OE8yMDuW3NNqumbroiUb/KdkrnyjZl5F6aSMXJEySq+OSoBboZJMwj2IWglc/7fuw==", + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.10.7.tgz", + "integrity": "sha512-yuoz/24mIfYit3neKqoE5NVs42Rj9A6A6SlkNPDfsy3v/Vh7SgYkU3cwiGyvwBGzIdhqL4/SWYo8H7YJLs0C+g==", "dev": true, "requires": { - "@lerna/global-options": "3.1.3", + "@lerna/global-options": "3.10.6", "dedent": "^0.7.0", "libnpm": "^2.0.1", "yargs": "^12.0.1" @@ -1066,13 +1066,13 @@ } }, "@lerna/command": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.10.0.tgz", - "integrity": "sha512-TTtCDQ5+bDdA/RnBuDtkfqzUV8Mr61KBHxEZL8YLAmHZtY/HsnNpZzbAZ0STPxcFB96dhxVWbRDGP+yBgRfemQ==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.10.6.tgz", + "integrity": "sha512-jPZswMZXOpAaIuSF5hrz+eaWQzbDrvwbrkCoRJKfiAHx7URAkE6MQe9DeAnqrTKMqwfg0RciSrZLc8kWYfrzCQ==", "dev": true, "requires": { "@lerna/child-process": "3.3.0", - "@lerna/package-graph": "3.10.0", + "@lerna/package-graph": "3.10.6", "@lerna/project": "3.10.0", "@lerna/validation-error": "3.6.0", "@lerna/write-log-file": "3.6.0", @@ -1170,13 +1170,13 @@ } }, "@lerna/create": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.10.0.tgz", - "integrity": "sha512-1EQbhyGx/J+gwlxFPecpmrztyEfBRm/sNei95UJlJWLuturSv2Ax2nCa49tcerbPlYhhlJ6lyintukL5STOzdg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.10.6.tgz", + "integrity": "sha512-OddQtGBHM2/eJONggLWoTE6275XGbnJ6dIVF+fLsKS93o4GC6g+qcc6Y7lUWHm5bfpeOwNOVKwj0tvqBZ6MgoA==", "dev": true, "requires": { "@lerna/child-process": "3.3.0", - "@lerna/command": "3.10.0", + "@lerna/command": "3.10.6", "@lerna/npm-conf": "3.7.0", "@lerna/validation-error": "3.6.0", "camelcase": "^4.1.0", @@ -1311,35 +1311,35 @@ } }, "@lerna/diff": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.10.0.tgz", - "integrity": "sha512-MU6P9uAND+dZ15Cm4onJakEYMC6xXZApLuPpWJf0kZtVoF2Feoo3mvQASdb17fe0jvvmWDS0RLCzq9Zhzrgm0A==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.10.6.tgz", + "integrity": "sha512-0MqFhosjrqsIdXiKIu7t3CiJELqiU9mkjFBhYPB7JruAzpPwjMXJnC6/Ur5/7LXJYYVpqGQwZI9ZaZlOYJhhrw==", "dev": true, "requires": { "@lerna/child-process": "3.3.0", - "@lerna/command": "3.10.0", + "@lerna/command": "3.10.6", "@lerna/validation-error": "3.6.0", "libnpm": "^2.0.1" } }, "@lerna/exec": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.10.1.tgz", - "integrity": "sha512-MM5/OMP4FrVH4PIlG+3xk3jpKq+trgu/eAPttaYZBHAumCOjrDVYdyk5O68+YLz+uLkM31ixTmsiAP9f77HTsg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.10.6.tgz", + "integrity": "sha512-cdHqaRBMYceJu8rZLO8b4ZeR27O+xKPHgzi13OOOfBJQjrTuacjMWyHgmpy8jWc/0f7QnTl4VsHks7VJ3UK+vw==", "dev": true, "requires": { - "@lerna/batch-packages": "3.10.0", + "@lerna/batch-packages": "3.10.6", "@lerna/child-process": "3.3.0", - "@lerna/command": "3.10.0", - "@lerna/filter-options": "3.10.1", + "@lerna/command": "3.10.6", + "@lerna/filter-options": "3.10.6", "@lerna/run-parallel-batches": "3.0.0", "@lerna/validation-error": "3.6.0" } }, "@lerna/filter-options": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.10.1.tgz", - "integrity": "sha512-34q7P0/AA+omVk9uwv99i+4qmj5uGuj383RzqIcK8JDYL0JSzlmW0+c4IkxunCfRrWft8OFhSwZdOOmXtDSDYg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.10.6.tgz", + "integrity": "sha512-r/dQbqN+RGFKZNn+DyWehswFmAkny/fkdMB2sRM2YVe7zRTtSl95YxD9DtdYnpJTG/jbOVICS/L5QJakrI6SSw==", "dev": true, "requires": { "@lerna/collect-updates": "3.10.1", @@ -1431,9 +1431,9 @@ } }, "@lerna/global-options": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.1.3.tgz", - "integrity": "sha512-LVeZU/Zgc0XkHdGMRYn+EmHfDmmYNwYRv3ta59iCVFXLVp7FRFWF7oB1ss/WRa9x/pYU0o6L8as/5DomLUGASA==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.10.6.tgz", + "integrity": "sha512-k5Xkq1M/uREFC2R9uwN5gcvIgjj4iOXo0YyeEXCMWBiW3j2GL9xN4d1MmAIcrYlAzVYh6kLlWaFWl/rNIneHIw==", "dev": true }, "@lerna/has-npm-version": { @@ -1447,13 +1447,13 @@ } }, "@lerna/import": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.10.0.tgz", - "integrity": "sha512-c8/s/ldaNVGuKnu600B3nbkwJTNElp1duJiZQ7EBChF+szbQBAiQUGNLvBbwClLBzVJhKTw6E4ku17HafQ4vqg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.10.6.tgz", + "integrity": "sha512-LlGxhfDhovoNoBJLF3PYd3j/G2GFTnfLh0V38+hBQ6lomMNJbjkACfiLVomQxPWWpYLk0GTlpWYR8YGv6L7Ifw==", "dev": true, "requires": { "@lerna/child-process": "3.3.0", - "@lerna/command": "3.10.0", + "@lerna/command": "3.10.6", "@lerna/prompt": "3.6.0", "@lerna/pulse-till-done": "3.7.1", "@lerna/validation-error": "3.6.0", @@ -1485,13 +1485,13 @@ } }, "@lerna/init": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.10.0.tgz", - "integrity": "sha512-+zU1A870OOOqy3MPLcEoicN6dnIGZv/q0aqCVRRfCHAICciaswuIvdX0uDJx0NrUe0sW40dzIllxuUA39nPqcw==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.10.6.tgz", + "integrity": "sha512-RIlEx+ofWLYRNjxCkkV3G0XQPM+/KA5RXRDb5wKQLYO1f+tZAaHoUh8fHDIvxGf/ohY/OIjYYGSsU+ysimfwiQ==", "dev": true, "requires": { "@lerna/child-process": "3.3.0", - "@lerna/command": "3.10.0", + "@lerna/command": "3.10.6", "fs-extra": "^7.0.0", "p-map": "^1.2.0", "write-json-file": "^2.3.0" @@ -1520,37 +1520,37 @@ } }, "@lerna/link": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.10.0.tgz", - "integrity": "sha512-uZvLxTSekqV8Kx0zMPgcxpTWyRkjnqnUzRiff9HQtOq+gBBifX079jGT7X73CO5eXFzp2TkOJtI1KNL0BNoNtA==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.10.6.tgz", + "integrity": "sha512-dwD6qftRWitgLDYbqtDrgO7c8uF5C0fHVew5M6gU5m9tBJidqd7cDwHv/bXboLEI63U7tt5y6LY+wEpYUFsBRw==", "dev": true, "requires": { - "@lerna/command": "3.10.0", - "@lerna/package-graph": "3.10.0", + "@lerna/command": "3.10.6", + "@lerna/package-graph": "3.10.6", "@lerna/symlink-dependencies": "3.10.0", "p-map": "^1.2.0", "slash": "^1.0.0" } }, "@lerna/list": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.10.1.tgz", - "integrity": "sha512-y2VwTeJ8tcQ0dmJJNhloGfhmCBUG3RXafqNkUVUG/ItoJlfzVniQOMdIDlkre86ZtnQv9yrB2vFaC2Vg++PklQ==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.10.6.tgz", + "integrity": "sha512-3ElQBj2dOB4uUkpsjC1bxdeZwEzRBuV1pBBs5E1LncwsZf7D9D99Z32fuZsDaCHpEMgHAD4/j8juI3/7m5dkaQ==", "dev": true, "requires": { - "@lerna/command": "3.10.0", - "@lerna/filter-options": "3.10.1", - "@lerna/listable": "3.10.0", + "@lerna/command": "3.10.6", + "@lerna/filter-options": "3.10.6", + "@lerna/listable": "3.10.6", "@lerna/output": "3.6.0" } }, "@lerna/listable": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.10.0.tgz", - "integrity": "sha512-95EwogHBqJxrXOCkf3DAZQAzJes+I668Lg5BJDotfp9eZeJAbgGl6GPz5U+szPq0PrYfK+2kJv9xNXVnbfCZAw==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.10.6.tgz", + "integrity": "sha512-F7ZuvesSgeuMiJf99eOum5p1MQGQStykcmHH1ek+LQRMiGGF1o3PkBxPvHTZBADGOFarek8bFA5TVmRAMX7NIw==", "dev": true, "requires": { - "@lerna/batch-packages": "3.10.0", + "@lerna/batch-packages": "3.10.6", "chalk": "^2.3.1", "columnify": "^1.5.4" } @@ -1632,9 +1632,9 @@ } }, "@lerna/npm-publish": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.10.5.tgz", - "integrity": "sha512-6wpgTfu5A5jJeB8RnH2n01HzfaB4Y9aKC0Tq0AAkw37PZ12LTgEL9I+ZZPqhUVFIFLB8/Ekpnj3AcKznJLG5xQ==", + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.10.7.tgz", + "integrity": "sha512-oU3/Q+eHC1fRjh7bk6Nn4tRD1OLR6XZVs3v+UWMWMrF4hVSV61pxcP5tpeI1n4gDQjSgh7seI4EzKVJe/WfraA==", "dev": true, "requires": { "@lerna/run-lifecycle": "3.10.5", @@ -1772,9 +1772,9 @@ } }, "@lerna/package-graph": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.10.0.tgz", - "integrity": "sha512-9LX8I8KxzCMjfNPWvN/CxHW51s89S3Mnx2EYsbo8c8Gh8I6InA6e+Xur6uuCyZ6/0LKqQ/cXwrP3J81A4nIDSQ==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.10.6.tgz", + "integrity": "sha512-mpIOJbhi+xLqT9BcUrLVD4We8WUdousQf/QndbEWl8DWAW1ethtRHVsCm9ufdBB3F9nj4PH/hqnDWWwqE+rS4w==", "dev": true, "requires": { "@lerna/validation-error": "3.6.0", @@ -1969,21 +1969,21 @@ } }, "@lerna/publish": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.10.5.tgz", - "integrity": "sha512-26wjTtRbcUXlG8Na7goI0X1trMYivbuLT1bAXHNvuDaHYs7iE6LRjU4NCTNAmrdVnqagHkTxMuGRFn3r1NgcKg==", + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.10.7.tgz", + "integrity": "sha512-Qd8pml2l9s6GIvNX1pTnia+Ddjsm9LF3pRRoOQeugAdv2IJNf45c/83AAEyE9M2ShG5VjgxEITNW4Lg49zipjQ==", "dev": true, "requires": { - "@lerna/batch-packages": "3.10.0", + "@lerna/batch-packages": "3.10.6", "@lerna/check-working-tree": "3.10.0", "@lerna/child-process": "3.3.0", "@lerna/collect-updates": "3.10.1", - "@lerna/command": "3.10.0", + "@lerna/command": "3.10.6", "@lerna/describe-ref": "3.10.0", "@lerna/log-packed": "3.6.0", "@lerna/npm-conf": "3.7.0", "@lerna/npm-dist-tag": "3.8.5", - "@lerna/npm-publish": "3.10.5", + "@lerna/npm-publish": "3.10.7", "@lerna/output": "3.6.0", "@lerna/pack-directory": "3.10.5", "@lerna/prompt": "3.6.0", @@ -1991,7 +1991,7 @@ "@lerna/run-lifecycle": "3.10.5", "@lerna/run-parallel-batches": "3.0.0", "@lerna/validation-error": "3.6.0", - "@lerna/version": "3.10.5", + "@lerna/version": "3.10.6", "figgy-pudding": "^3.5.1", "fs-extra": "^7.0.0", "libnpm": "^2.0.1", @@ -2079,14 +2079,14 @@ } }, "@lerna/run": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.10.1.tgz", - "integrity": "sha512-g9YIcpk87Gok+zjicru/KsuZ1lcyuG5oERyAii3RSmpLaiwTh/SOSnxilrvDOYWwxYU5rPzvaCalkQI/i31Itw==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.10.6.tgz", + "integrity": "sha512-KS2lWbu/8WUUscQPi9U8sPO6yYpzf/0GmODjpruR1nRi1u/tuncdjTiG+hjGAeFC1BD7YktT9Za6imIpE8RXmA==", "dev": true, "requires": { - "@lerna/batch-packages": "3.10.0", - "@lerna/command": "3.10.0", - "@lerna/filter-options": "3.10.1", + "@lerna/batch-packages": "3.10.6", + "@lerna/command": "3.10.6", + "@lerna/filter-options": "3.10.6", "@lerna/npm-run-script": "3.10.0", "@lerna/output": "3.6.0", "@lerna/run-parallel-batches": "3.0.0", @@ -2203,16 +2203,16 @@ } }, "@lerna/version": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.10.5.tgz", - "integrity": "sha512-I6KynsrWvtusylggw+XmlfUud26ncfUctbn8hUQsofkxiouuElx1fUU4rEsOaonxvNk09bwlsGmfbIFsPeN6Hg==", + "version": "3.10.6", + "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.10.6.tgz", + "integrity": "sha512-77peW2ROlHHl1e/tHBUmhpb8tsO6CIdlx34XapZhUuIVykrkOuqVFFxqMecrGG8SJe0e3l1G+Fah7bJTQcG0kw==", "dev": true, "requires": { - "@lerna/batch-packages": "3.10.0", + "@lerna/batch-packages": "3.10.6", "@lerna/check-working-tree": "3.10.0", "@lerna/child-process": "3.3.0", "@lerna/collect-updates": "3.10.1", - "@lerna/command": "3.10.0", + "@lerna/command": "3.10.6", "@lerna/conventional-commits": "3.10.0", "@lerna/output": "3.6.0", "@lerna/prompt": "3.6.0", @@ -12497,26 +12497,26 @@ "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" }, "lerna": { - "version": "3.10.5", - "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.10.5.tgz", - "integrity": "sha512-rJ67oqEiF8AVw+9phKbGkC0k0oqu1rdnmzrIfVS40EQCwEtzBC1ABX1886PBV0N40Pt9wCy6a0Jhrd+PV4IiIQ==", + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.10.7.tgz", + "integrity": "sha512-ha/dehl/L3Nw0pbdir5z6Hrv2oYBg5ym2fTcuk8HCLe7Zdb/ylIHdrgW8CU9eTVZkwr4et8RdVtxFA/+xa65/Q==", "dev": true, "requires": { - "@lerna/add": "3.10.5", - "@lerna/bootstrap": "3.10.5", - "@lerna/changed": "3.10.5", - "@lerna/clean": "3.10.1", - "@lerna/cli": "3.10.0", - "@lerna/create": "3.10.0", - "@lerna/diff": "3.10.0", - "@lerna/exec": "3.10.1", - "@lerna/import": "3.10.0", - "@lerna/init": "3.10.0", - "@lerna/link": "3.10.0", - "@lerna/list": "3.10.1", - "@lerna/publish": "3.10.5", - "@lerna/run": "3.10.1", - "@lerna/version": "3.10.5", + "@lerna/add": "3.10.6", + "@lerna/bootstrap": "3.10.6", + "@lerna/changed": "3.10.6", + "@lerna/clean": "3.10.6", + "@lerna/cli": "3.10.7", + "@lerna/create": "3.10.6", + "@lerna/diff": "3.10.6", + "@lerna/exec": "3.10.6", + "@lerna/import": "3.10.6", + "@lerna/init": "3.10.6", + "@lerna/link": "3.10.6", + "@lerna/list": "3.10.6", + "@lerna/publish": "3.10.7", + "@lerna/run": "3.10.6", + "@lerna/version": "3.10.6", "import-local": "^1.0.0", "libnpm": "^2.0.1" } @@ -12707,9 +12707,9 @@ } }, "libnpmpublish": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-1.1.0.tgz", - "integrity": "sha512-mQ3LT2EWlpJ6Q8mgHTNqarQVCgcY32l6xadPVPMcjWLtVLz7II4WlWkzlbYg1nHGAf+xyABDwS+3aNUiRLkyaA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-1.1.1.tgz", + "integrity": "sha512-nefbvJd/wY38zdt+b9SHL6171vqBrMtZ56Gsgfd0duEKb/pB8rDT4/ObUQLrHz1tOfht1flt2zM+UGaemzAG5g==", "dev": true, "requires": { "aproba": "^2.0.0", @@ -14392,9 +14392,9 @@ } }, "npm-registry-fetch": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.8.0.tgz", - "integrity": "sha512-hrw8UMD+Nob3Kl3h8Z/YjmKamb1gf7D1ZZch2otrIXM3uFLB5vjEY6DhMlq80z/zZet6eETLbOXcuQudCB3Zpw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.9.0.tgz", + "integrity": "sha512-srwmt8YhNajAoSAaDWndmZgx89lJwIZ1GWxOuckH4Coek4uHv5S+o/l9FLQe/awA+JwTnj4FJHldxhlXdZEBmw==", "dev": true, "requires": { "JSONStream": "^1.3.4", @@ -14895,9 +14895,9 @@ } }, "pacote": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.3.0.tgz", - "integrity": "sha512-uy5xghB5wUtmFS+uNhQGhlsIF9rfsfxw6Zsu2VpmSz4/f+8D2+5V1HwjHdSn7W6aQTrxNNmmoUF5qNE10/EVdA==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.4.1.tgz", + "integrity": "sha512-YKSRsQqmeHxgra0KCdWA2FtVxDPUlBiCdmew+mSe44pzlx5t1ViRMWiQg18T+DREA+vSqYfKzynaToFR4hcKHw==", "dev": true, "requires": { "bluebird": "^3.5.3", @@ -18519,9 +18519,9 @@ "dev": true }, "smart-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz", - "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.2.tgz", + "integrity": "sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw==", "dev": true }, "snapdragon": { @@ -18635,13 +18635,13 @@ } }, "socks": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.2.tgz", - "integrity": "sha512-g6wjBnnMOZpE0ym6e0uHSddz9p3a+WsBaaYQaBaSCJYvrC4IXykQR9MNGjLQf38e9iIIhp3b1/Zk8YZI3KGJ0Q==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.3.tgz", + "integrity": "sha512-+2r83WaRT3PXYoO/1z+RDEBE7Z2f9YcdQnJ0K/ncXXbV5gJ6wYfNAebYFYiiUjM6E4JyXnPY8cimwyvFYHVUUA==", "dev": true, "requires": { "ip": "^1.1.5", - "smart-buffer": "^4.0.1" + "smart-buffer": "4.0.2" } }, "socks-proxy-agent": { diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index cb3feee6c3f..3754a89f520 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -84,7 +84,7 @@ "grunt-checktextdomain": "1.0.1", "grunt-wp-i18n": "1.0.3", "husky": "1.3.1", - "lerna": "3.10.5", + "lerna": "3.10.7", "locutus": "^2.0.10", "mini-css-extract-plugin": "0.5.0", "node-sass": "4.11.0", From 974d263f69d58f5c8517e361477b2eb8a7509d78 Mon Sep 17 00:00:00 2001 From: Timmy Crawford Date: Wed, 30 Jan 2019 10:51:03 -0800 Subject: [PATCH 72/91] Release/v0.6.0 (https://github.com/woocommerce/woocommerce-admin/pull/1417) * Bump versions to 0.6.0 * Update docs * Updates after running build. --- .../docs/components/packages/flag.md | 32 ++++++------------- .../docs/components/packages/summary.md | 17 +++++++--- plugins/woocommerce-admin/package-lock.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- plugins/woocommerce-admin/wc-admin.php | 2 +- 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/plugins/woocommerce-admin/docs/components/packages/flag.md b/plugins/woocommerce-admin/docs/components/packages/flag.md index 2c5c62dba88..a3c64007c30 100644 --- a/plugins/woocommerce-admin/docs/components/packages/flag.md +++ b/plugins/woocommerce-admin/docs/components/packages/flag.md @@ -1,9 +1,9 @@ `Flag` (component) ================== -Use the `Flag` component to display a country's flag. - +Use the `Flag` component to display a country's flag using the operating system's emojis. + React component. Props ----- @@ -22,27 +22,6 @@ Two letter, three letter or three digit country code. An order can be passed instead of `code` and the code will automatically be pulled from the billing or shipping data. -### `round` - -- Type: Boolean -- Default: `true` - -True to display a rounded flag. - -### `height` - -- Type: Number -- Default: `24` - -Flag image height. - -### `width` - -- Type: Number -- Default: `24` - -Flag image width. - ### `className` - Type: String @@ -50,3 +29,10 @@ Flag image width. Additional CSS classes. +### `size` + +- Type: Number +- Default: null + +Supply a font size to be applied to the emoji flag. + diff --git a/plugins/woocommerce-admin/docs/components/packages/summary.md b/plugins/woocommerce-admin/docs/components/packages/summary.md index f8e217e4020..804c7d4eeea 100644 --- a/plugins/woocommerce-admin/docs/components/packages/summary.md +++ b/plugins/woocommerce-admin/docs/components/packages/summary.md @@ -12,17 +12,17 @@ Props ### `children` - **Required** -- Type: ReactNode +- Type: Function - Default: null -A list of ``s +A function returning a list of ``s ### `label` - Type: String -- Default: null +- Default: `__( 'Performance Indicators', 'wc-admin' )` -An optional label of this group, read to screen reader users. Defaults to "Performance Indicators". +An optional label of this group, read to screen reader users. `SummaryNumber` (component) =========================== @@ -46,7 +46,7 @@ If omitted, no change value will display. ### `href` - Type: String -- Default: `'/analytics'` +- Default: `''` An internal link to the report focused on this number. @@ -109,6 +109,13 @@ A boolean used to show a highlight style on this number. A string or number value to display - a string is allowed so we can accept currency formatting. +### `onLinkClickCallback` + +- Type: Function +- Default: `noop` + +A function to be called after a SummaryNumber, rendered as a link, is clicked. + `SummaryListPlaceholder` (component) ==================================== diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index d89aa78bcdf..fe3cea0b11d 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -1,6 +1,6 @@ { "name": "wc-admin", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 3754a89f520..04a2e9be1a7 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -1,6 +1,6 @@ { "name": "wc-admin", - "version": "0.5.0", + "version": "0.6.0", "main": "js/index.js", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/plugins/woocommerce-admin/wc-admin.php b/plugins/woocommerce-admin/wc-admin.php index b324db51027..b933745e927 100755 --- a/plugins/woocommerce-admin/wc-admin.php +++ b/plugins/woocommerce-admin/wc-admin.php @@ -7,7 +7,7 @@ * Author URI: https://woocommerce.com/ * Text Domain: wc-admin * Domain Path: /languages - * Version: 0.5.0 + * Version: 0.6.0 * * @package WC_Admin */ From 43c753011a147ad4bfc154ab7081363a9a686dcb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 13:53:25 -0500 Subject: [PATCH 73/91] Update dependency eslint to v5.12.1 (https://github.com/woocommerce/woocommerce-admin/pull/1376) --- plugins/woocommerce-admin/package-lock.json | 26 ++++++++++----------- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index fe3cea0b11d..714fcead56e 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -7623,9 +7623,9 @@ } }, "eslint": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.12.0.tgz", - "integrity": "sha512-LntwyPxtOHrsJdcSwyQKVtHofPHdv+4+mFwEe91r2V13vqpM8yLr7b1sW+Oo/yheOPkWYsYlYJCkzlFAt8KV7g==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.12.1.tgz", + "integrity": "sha512-54NV+JkTpTu0d8+UYSA8mMKAG4XAsaOrozA9rCW7tgneg1mevcL7wIotPC+fZ0SkWwdhNqoXoxnQCTBp7UvTsg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -7668,9 +7668,9 @@ }, "dependencies": { "acorn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.5.tgz", - "integrity": "sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz", + "integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==", "dev": true }, "acorn-jsx": { @@ -7781,9 +7781,9 @@ "dev": true }, "slice-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", - "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", "dev": true, "requires": { "ansi-styles": "^3.2.0", @@ -7809,14 +7809,14 @@ } }, "table": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/table/-/table-5.2.0.tgz", - "integrity": "sha512-hAdBBAMCZl4/U3eQhsPN2Z8wRJC98lpRhDW2I86VQbPBqyj4E681VhvUkfb90qUJ4rnRfu8t4/8SGHPsAH1ygg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/table/-/table-5.2.2.tgz", + "integrity": "sha512-f8mJmuu9beQEDkKHLzOv4VxVYlU68NpdzjbGPl69i4Hx0sTopJuNxuzJd17iV2h24dAfa93u794OnDA5jqXvfQ==", "dev": true, "requires": { "ajv": "^6.6.1", "lodash": "^4.17.11", - "slice-ansi": "2.0.0", + "slice-ansi": "^2.0.0", "string-width": "^2.1.1" } } diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 04a2e9be1a7..51c4fcd9b5d 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -73,7 +73,7 @@ "deasync": "0.1.14", "deep-freeze": "0.0.1", "docsify-cli": "4.3.0", - "eslint": "5.12.0", + "eslint": "5.12.1", "eslint-config-wpcalypso": "4.0.1", "eslint-loader": "2.1.1", "eslint-plugin-jest": "22.2.1", From 40cd3545fe6e37fc09bb78b89593f9c60209e715 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 13:59:32 -0500 Subject: [PATCH 74/91] Update dependency eslint-plugin-react to v7.12.4 (https://github.com/woocommerce/woocommerce-admin/pull/1418) --- plugins/woocommerce-admin/package-lock.json | 6 +++--- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 714fcead56e..ef4f8be84a5 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -7864,9 +7864,9 @@ } }, "eslint-plugin-react": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.3.tgz", - "integrity": "sha512-WTIA3cS8OzkPeCi4KWuPmjR33lgG9r9Y/7RmnLTRw08MZKgAfnK/n3BO4X0S67MPkVLazdfCNT/XWqcDu4BLTA==", + "version": "7.12.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz", + "integrity": "sha512-1puHJkXJY+oS1t467MjbqjvX53uQ05HXwjqDgdbGBqf5j9eeydI54G3KwiJmWciQ0HTBacIKw2jgwSBSH3yfgQ==", "dev": true, "requires": { "array-includes": "^3.0.3", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 51c4fcd9b5d..2bee5507c35 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -78,7 +78,7 @@ "eslint-loader": "2.1.1", "eslint-plugin-jest": "22.2.1", "eslint-plugin-jsx-a11y": "6.1.2", - "eslint-plugin-react": "7.12.3", + "eslint-plugin-react": "7.12.4", "eslint-plugin-wpcalypso": "4.0.2", "grunt": "1.0.3", "grunt-checktextdomain": "1.0.1", From 099e8a60afa7f99950df7c17d42e7132d3862bc9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 14:20:36 -0500 Subject: [PATCH 75/91] Update dependency eslint-plugin-jsx-a11y to v6.2.0 (https://github.com/woocommerce/woocommerce-admin/pull/1416) --- plugins/woocommerce-admin/package-lock.json | 18 +++++++++++++----- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index ef4f8be84a5..89f2ab6257d 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -7848,19 +7848,27 @@ "dev": true }, "eslint-plugin-jsx-a11y": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.2.tgz", - "integrity": "sha512-7gSSmwb3A+fQwtw0arguwMdOdzmKUgnUcbSNlo+GjKLAQFuC2EZxWqG9XHRI8VscBJD5a8raz3RuxQNFW+XJbw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.0.tgz", + "integrity": "sha512-KpibpIdKT0nbDG7G/ILWoZoN5G9cj3h1IHARxzvHk5Nt2LQ7oPUutbO9QbT1FKNHLPZB6BaoA1ASSLOiJZVEUQ==", "dev": true, "requires": { "aria-query": "^3.0.0", "array-includes": "^3.0.3", "ast-types-flow": "^0.0.7", - "axobject-query": "^2.0.1", + "axobject-query": "^2.0.2", "damerau-levenshtein": "^1.0.4", - "emoji-regex": "^6.5.1", + "emoji-regex": "^7.0.2", "has": "^1.0.3", "jsx-ast-utils": "^2.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + } } }, "eslint-plugin-react": { diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 2bee5507c35..a9508c3a34f 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -77,7 +77,7 @@ "eslint-config-wpcalypso": "4.0.1", "eslint-loader": "2.1.1", "eslint-plugin-jest": "22.2.1", - "eslint-plugin-jsx-a11y": "6.1.2", + "eslint-plugin-jsx-a11y": "6.2.0", "eslint-plugin-react": "7.12.4", "eslint-plugin-wpcalypso": "4.0.2", "grunt": "1.0.3", From ead7ca05236ed7fedee21e7116b9ff3b8b115d09 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 15:13:31 -0500 Subject: [PATCH 76/91] Pin dependency locutus to 2.0.10 (https://github.com/woocommerce/woocommerce-admin/pull/1408) --- plugins/woocommerce-admin/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index a9508c3a34f..412d0258cba 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -85,7 +85,7 @@ "grunt-wp-i18n": "1.0.3", "husky": "1.3.1", "lerna": "3.10.7", - "locutus": "^2.0.10", + "locutus": "2.0.10", "mini-css-extract-plugin": "0.5.0", "node-sass": "4.11.0", "node-watch": "0.6.0", From e18837e255f4384ad3b5e9f46321dc2d83fc1a82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 15:25:19 -0500 Subject: [PATCH 77/91] Update dependency autoprefixer to v9.4.7 (https://github.com/woocommerce/woocommerce-admin/pull/1374) --- plugins/woocommerce-admin/package-lock.json | 41 ++++++++++++++++----- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 89f2ab6257d..5a2064dc601 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -3848,23 +3848,46 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "autoprefixer": { - "version": "9.4.5", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.4.5.tgz", - "integrity": "sha512-M602C0ZxzFpJKqD4V6eq2j+K5CkzlhekCrcQupJmAOrPEZjWJyj/wSeo6qRSNoN6M3/9mtLPQqTTrABfReytQg==", + "version": "9.4.7", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.4.7.tgz", + "integrity": "sha512-qS5wW6aXHkm53Y4z73tFGsUhmZu4aMPV9iHXYlF0c/wxjknXNHuj/1cIQb+6YH692DbJGGWcckAXX+VxKvahMA==", "dev": true, "requires": { - "browserslist": "^4.4.0", - "caniuse-lite": "^1.0.30000928", + "browserslist": "^4.4.1", + "caniuse-lite": "^1.0.30000932", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", - "postcss": "^7.0.11", + "postcss": "^7.0.14", "postcss-value-parser": "^3.3.1" }, "dependencies": { + "browserslist": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", + "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000929", + "electron-to-chromium": "^1.3.103", + "node-releases": "^1.1.3" + } + }, + "caniuse-lite": { + "version": "1.0.30000932", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz", + "integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.108", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.108.tgz", + "integrity": "sha512-/QI4hMpAh48a1Sea6PALGv+kuVne9A2EWGd8HrWHMdYhIzGtbhVVHh6heL5fAzGaDnZuPyrlWJRl8WPm4RyiQQ==", + "dev": true + }, "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 412d0258cba..724a600d015 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -59,7 +59,7 @@ "@wordpress/jest-preset-default": "3.0.3", "@wordpress/postcss-themes": "1.0.4", "ast-types": "0.11.7", - "autoprefixer": "9.4.5", + "autoprefixer": "9.4.7", "babel-core": "7.0.0-bridge.0", "babel-eslint": "10.0.1", "babel-loader": "8.0.5", From f128716d28e7e19a3b3cbfbe75fa29ac362a4769 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 15:46:29 -0500 Subject: [PATCH 78/91] Update babel monorepo (https://github.com/woocommerce/woocommerce-admin/pull/1373) --- plugins/woocommerce-admin/package-lock.json | 34 +++++++++++++------ plugins/woocommerce-admin/package.json | 4 +-- .../packages/components/package.json | 2 +- .../packages/csv-export/package.json | 2 +- .../packages/number/package.json | 2 +- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index 5a2064dc601..c1d97802dc9 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -89,12 +89,24 @@ } }, "@babel/helper-builder-react-jsx": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0.tgz", - "integrity": "sha512-ebJ2JM6NAKW0fQEqN8hOLxK84RbRz9OkUhGS/Xd5u56ejMfVbayJ4+LykERZCOUM6faa6Fp3SZNX3fcT16MKHw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", + "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", "requires": { - "@babel/types": "^7.0.0", + "@babel/types": "^7.3.0", "esutils": "^2.0.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", + "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.10", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-call-delegate": { @@ -548,11 +560,11 @@ } }, "@babel/plugin-transform-react-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.2.0.tgz", - "integrity": "sha512-h/fZRel5wAfCqcKgq3OhbmYaReo7KkoJBpt8XnvpS7wqaNMqtw5xhxutzcm35iMUWucfAdT/nvGTsWln0JTg2Q==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", + "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", "requires": { - "@babel/helper-builder-react-jsx": "^7.0.0", + "@babel/helper-builder-react-jsx": "^7.3.0", "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" } @@ -685,9 +697,9 @@ } }, "@babel/runtime-corejs2": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.2.0.tgz", - "integrity": "sha512-kPfmKoRI8Hpo5ZJGACWyrc9Eq1j3ZIUpUAQT2yH045OuYpccFJ9kYA/eErwzOM2jeBG1sC8XX1nl1EArtuM8tg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.3.1.tgz", + "integrity": "sha512-YpO13776h3e6Wy8dl2J8T9Qwlvopr+b4trCEhHE+yek6yIqV8sx6g3KozdHMbXeBpjosbPi+Ii5Z7X9oXFHUKA==", "requires": { "core-js": "^2.5.7", "regenerator-runtime": "^0.12.0" diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 724a600d015..3f61d5d082e 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -49,8 +49,8 @@ "@babel/cli": "7.2.3", "@babel/core": "7.2.2", "@babel/plugin-transform-async-to-generator": "7.2.0", - "@babel/plugin-transform-react-jsx": "7.2.0", - "@babel/runtime-corejs2": "7.2.0", + "@babel/plugin-transform-react-jsx": "7.3.0", + "@babel/runtime-corejs2": "7.3.1", "@wordpress/babel-plugin-import-jsx-pragma": "1.1.2", "@wordpress/babel-plugin-makepot": "2.1.2", "@wordpress/babel-preset-default": "3.0.1", diff --git a/plugins/woocommerce-admin/packages/components/package.json b/plugins/woocommerce-admin/packages/components/package.json index 46070b6560d..bbc06a370c9 100644 --- a/plugins/woocommerce-admin/packages/components/package.json +++ b/plugins/woocommerce-admin/packages/components/package.json @@ -21,7 +21,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime-corejs2": "7.2.0", + "@babel/runtime-corejs2": "7.3.1", "@woocommerce/csv-export": "^1.0.3", "@woocommerce/currency": "^1.1.0", "@woocommerce/date": "^1.0.6", diff --git a/plugins/woocommerce-admin/packages/csv-export/package.json b/plugins/woocommerce-admin/packages/csv-export/package.json index c917f8e0c28..339462e4ffc 100644 --- a/plugins/woocommerce-admin/packages/csv-export/package.json +++ b/plugins/woocommerce-admin/packages/csv-export/package.json @@ -21,7 +21,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime-corejs2": "7.2.0", + "@babel/runtime-corejs2": "7.3.1", "browser-filesaver": "^1.1.1", "moment": "^2.22.2" }, diff --git a/plugins/woocommerce-admin/packages/number/package.json b/plugins/woocommerce-admin/packages/number/package.json index e9bb5388c32..d379317a640 100644 --- a/plugins/woocommerce-admin/packages/number/package.json +++ b/plugins/woocommerce-admin/packages/number/package.json @@ -20,7 +20,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime-corejs2": "7.1.5", + "@babel/runtime-corejs2": "7.3.1", "locutus": "^2.0.10", "lodash": "^4.17.11" }, From 85a46252df39b717978dceb42d6787a74b7b56e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 15:59:19 -0500 Subject: [PATCH 79/91] Update dependency core-js to v2.6.3 (https://github.com/woocommerce/woocommerce-admin/pull/1375) --- plugins/woocommerce-admin/package-lock.json | 6 +++--- plugins/woocommerce-admin/package.json | 2 +- plugins/woocommerce-admin/packages/components/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index c1d97802dc9..c1858b443f6 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -6136,9 +6136,9 @@ } }, "core-js": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.2.tgz", - "integrity": "sha512-NdBPF/RVwPW6jr0NCILuyN9RiqLo2b1mddWHkUL+VnvcB7dzlnBJ1bXYntjpTGOgkZiiLWj2JxmOr7eGE3qK6g==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.3.tgz", + "integrity": "sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ==" }, "core-util-is": { "version": "1.0.2", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 3f61d5d082e..9f6a56eac79 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -122,7 +122,7 @@ "@wordpress/viewport": "^2.0.7", "browser-filesaver": "^1.1.1", "classnames": "^2.2.5", - "core-js": "2.6.2", + "core-js": "2.6.3", "d3-array": "^2.0.0", "d3-axis": "^1.0.12", "d3-format": "^1.3.2", diff --git a/plugins/woocommerce-admin/packages/components/package.json b/plugins/woocommerce-admin/packages/components/package.json index bbc06a370c9..893f5333579 100644 --- a/plugins/woocommerce-admin/packages/components/package.json +++ b/plugins/woocommerce-admin/packages/components/package.json @@ -35,7 +35,7 @@ "@wordpress/keycodes": "2.0.5", "@wordpress/viewport": "^2.0.7", "classnames": "^2.2.5", - "core-js": "2.6.2", + "core-js": "2.6.3", "d3-array": "^2.0.0", "d3-axis": "^1.0.12", "d3-format": "^1.3.2", From 713d64c97a860dc2e5a20858d88fba2fbf7bf97a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Wed, 30 Jan 2019 16:15:54 -0500 Subject: [PATCH 80/91] Lock file maintenance (https://github.com/woocommerce/woocommerce-admin/pull/1362) --- plugins/woocommerce-admin/package-lock.json | 2768 +++++++++---------- 1 file changed, 1346 insertions(+), 1422 deletions(-) diff --git a/plugins/woocommerce-admin/package-lock.json b/plugins/woocommerce-admin/package-lock.json index c1858b443f6..eb6859c8e24 100644 --- a/plugins/woocommerce-admin/package-lock.json +++ b/plugins/woocommerce-admin/package-lock.json @@ -60,11 +60,11 @@ } }, "@babel/generator": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.2.2.tgz", - "integrity": "sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz", + "integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==", "requires": { - "@babel/types": "^7.2.2", + "@babel/types": "^7.3.0", "jsesc": "^2.5.1", "lodash": "^4.17.10", "source-map": "^0.5.0", @@ -95,18 +95,6 @@ "requires": { "@babel/types": "^7.3.0", "esutils": "^2.0.0" - }, - "dependencies": { - "@babel/types": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", - "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.10", - "to-fast-properties": "^2.0.0" - } - } } }, "@babel/helper-call-delegate": { @@ -266,13 +254,13 @@ } }, "@babel/helpers": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.2.0.tgz", - "integrity": "sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz", + "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==", "requires": { "@babel/template": "^7.1.2", "@babel/traverse": "^7.1.5", - "@babel/types": "^7.2.0" + "@babel/types": "^7.3.0" } }, "@babel/highlight": { @@ -286,9 +274,9 @@ } }, "@babel/parser": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.3.tgz", - "integrity": "sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA==" + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz", + "integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.2.0", @@ -310,9 +298,9 @@ } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz", + "integrity": "sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==", "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-object-rest-spread": "^7.2.0" @@ -532,6 +520,14 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz", + "integrity": "sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw==", + "requires": { + "regexp-tree": "^0.1.0" + } + }, "@babel/plugin-transform-new-target": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz", @@ -641,18 +637,19 @@ } }, "@babel/preset-env": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.2.3.tgz", - "integrity": "sha512-AuHzW7a9rbv5WXmvGaPX7wADxFkZIqKlbBh1dmZUQp4iwiPpkE/Qnrji6SC4UQCQzvWY/cpHET29eUhXS9cLPw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.3.1.tgz", + "integrity": "sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ==", "requires": { "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-async-generator-functions": "^7.2.0", "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.3.1", "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", "@babel/plugin-proposal-unicode-property-regex": "^7.2.0", "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", "@babel/plugin-syntax-object-rest-spread": "^7.2.0", "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", "@babel/plugin-transform-arrow-functions": "^7.2.0", @@ -672,6 +669,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.2.0", "@babel/plugin-transform-modules-systemjs": "^7.2.0", "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.3.0", "@babel/plugin-transform-new-target": "^7.0.0", "@babel/plugin-transform-object-super": "^7.2.0", "@babel/plugin-transform-parameters": "^7.2.0", @@ -689,9 +687,9 @@ } }, "@babel/runtime": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", - "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", + "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", "requires": { "regenerator-runtime": "^0.12.0" } @@ -732,9 +730,9 @@ } }, "@babel/types": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.2.2.tgz", - "integrity": "sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", + "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", "requires": { "esutils": "^2.0.2", "lodash": "^4.17.10", @@ -840,45 +838,6 @@ "chalk": "^2.3.1", "execa": "^1.0.0", "strong-log-transformer": "^2.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } } }, "@lerna/clean": { @@ -907,161 +866,6 @@ "dedent": "^0.7.0", "libnpm": "^2.0.1", "yargs": "^12.0.1" - }, - "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } } }, "@lerna/collect-updates": { @@ -1093,45 +897,6 @@ "is-ci": "^1.0.10", "libnpm": "^2.0.1", "lodash": "^4.17.5" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } } }, "@lerna/conventional-commits": { @@ -1161,15 +926,6 @@ "universalify": "^0.1.0" } }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -1206,6 +962,12 @@ "whatwg-url": "^7.0.0" }, "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, "dir-glob": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", @@ -1565,6 +1327,33 @@ "@lerna/batch-packages": "3.10.6", "chalk": "^2.3.1", "columnify": "^1.5.4" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "columnify": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", + "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "dev": true, + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } } }, "@lerna/log-packed": { @@ -1577,6 +1366,33 @@ "columnify": "^1.5.4", "has-unicode": "^2.0.1", "libnpm": "^2.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "columnify": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", + "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "dev": true, + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } } }, "@lerna/npm-conf": { @@ -2270,36 +2086,36 @@ "dev": true }, "@tannin/compile": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.0.1.tgz", - "integrity": "sha512-ymd9icvnkQin8UG4eRU3+xBc7gqTn/Kv5+EMY3ALWVwIl6j/7McWbCkxB8MgU40UaHJk8kLCk06wiKszXLdXWQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.0.2.tgz", + "integrity": "sha512-Zv4CtKcI5tmo5epgRwFR3uPrNuCzCuFJOFhONmEanNFSVt/Ck/rV4fdkOJ0bJPxV/AwR5fcmxDx4Xxd/GDvi6g==", "requires": { - "@tannin/evaluate": "^1.0.0", - "@tannin/postfix": "^1.0.0" + "@tannin/evaluate": "^1.1.0", + "@tannin/postfix": "^1.0.1" } }, "@tannin/evaluate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.0.0.tgz", - "integrity": "sha512-gO7YbJsD8sj5/nqUbFZv71Meu2++D9n4DZov/cWwp3YJbBwKShPlWwwlXr/0vz4vuxm/gys+3NiGbZkmhlXf0Q==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.1.0.tgz", + "integrity": "sha512-plrVqbuiqh6tWpAKznsXkCT5t4cmTLinfrB3AmX6zDduJkFmKb55n2JBdSB6D6SFvtJHtiFCmp4CUrn9dCNlqA==" }, "@tannin/plural-forms": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.0.1.tgz", - "integrity": "sha512-SXutT+XLbMOECvmWDBSqIOHhS5hzWG9875HCFGKYgp8ghGPrJ4HZ325Xc0hsRThdjgrWMEQixlbpWl4SXOQTig==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.0.2.tgz", + "integrity": "sha512-LNO8NwyVRSDOL3yDWo7Oao1Guceqr6KD0nOqR1t2mEPw21u4Tscvb0UqnAZ+IiXRzZsymPgeECss5JaEXoq30w==", "requires": { - "@tannin/compile": "^1.0.0" + "@tannin/compile": "^1.0.2" } }, "@tannin/postfix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.0.0.tgz", - "integrity": "sha512-59/mWwU7sXHfoU2kI3RcWRki2Jjbz5nEVJNBN4MUyIhPjXTebAcZqgsQACvlk+sjKVOTMEMHcrFrKQbaxz/1Dw==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.0.1.tgz", + "integrity": "sha512-y+h7tNlxDPDrH/TrSw1wCSm6FoEAY8FrbUxYng3BMSYBTUsX1utLooizk9v8J1yy6F9AioXNnPZ1qiu2vsa08Q==" }, "@types/node": { - "version": "10.12.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", - "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + "version": "10.12.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz", + "integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==" }, "@webassemblyjs/ast": { "version": "1.7.11", @@ -2818,9 +2634,9 @@ } }, "@wordpress/rich-text": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-3.0.5.tgz", - "integrity": "sha512-QgufyH481QN7cJCnUJ5qlIC22tLedmFe5D+QoIICmCvWHjxBBfDSe9IFvEz2RW5t0i8hrGAmIl962gLb1OY0KQ==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-3.0.7.tgz", + "integrity": "sha512-3zRyfVksOqt5BgrYRbcyVoXc4MXejBv9PjaXzcd5ZbzKm1dYt9J6mAPCiuErDCuNN2XF0LfSb5sXuVSdklHAqg==", "requires": { "@babel/runtime": "^7.0.0", "@wordpress/compose": "^3.0.1", @@ -3071,14 +2887,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } } } }, @@ -3191,9 +2999,9 @@ }, "dependencies": { "acorn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.5.tgz", - "integrity": "sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==" + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz", + "integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==" } } }, @@ -3289,9 +3097,9 @@ } }, "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" }, "ansi-regex": { "version": "3.0.0", @@ -3873,29 +3681,6 @@ "postcss-value-parser": "^3.3.1" }, "dependencies": { - "browserslist": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", - "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000929", - "electron-to-chromium": "^1.3.103", - "node-releases": "^1.1.3" - } - }, - "caniuse-lite": { - "version": "1.0.30000932", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz", - "integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.108", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.108.tgz", - "integrity": "sha512-/QI4hMpAh48a1Sea6PALGv+kuVne9A2EWGd8HrWHMdYhIzGtbhVVHh6heL5fAzGaDnZuPyrlWJRl8WPm4RyiQQ==", - "dev": true - }, "postcss": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", @@ -3953,6 +3738,11 @@ "js-tokens": "^3.0.2" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -3975,6 +3765,14 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -4105,6 +3903,46 @@ "find-up": "^2.1.0", "istanbul-lib-instrument": "^1.10.1", "test-exclude": "^4.2.1" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + } } }, "babel-plugin-jest-hoist": { @@ -4474,6 +4312,14 @@ "string-width": "^2.0.0", "term-size": "^1.2.0", "widest-line": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } }, "brace-expansion": { @@ -4603,12 +4449,12 @@ } }, "browserslist": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.0.tgz", - "integrity": "sha512-tQkHS8VVxWbrjnNDXgt7/+SuPJ7qDvD0Y2e6bLtoQluR2SPvlmPUcfcU75L1KAalhqULlIFJlJ6BDfnYyJxJsw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", + "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", "requires": { - "caniuse-lite": "^1.0.30000928", - "electron-to-chromium": "^1.3.100", + "caniuse-lite": "^1.0.30000929", + "electron-to-chromium": "^1.3.103", "node-releases": "^1.1.3" } }, @@ -4703,14 +4549,6 @@ "ssri": "^5.2.4", "unique-filename": "^1.1.0", "y18n": "^4.0.0" - }, - "dependencies": { - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - } } }, "cache-base": { @@ -4772,9 +4610,9 @@ "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" }, "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" }, "camelcase-keys": { "version": "4.2.0", @@ -4784,12 +4622,19 @@ "camelcase": "^4.1.0", "map-obj": "^2.0.0", "quick-lru": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } } }, "caniuse-lite": { - "version": "1.0.30000928", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000928.tgz", - "integrity": "sha512-aSpMWRXL6ZXNnzm8hgE4QDLibG5pVJ2Ujzsuj3icazlIkxXkPXtL+BWnMx6FBkWmkZgBHGUxPZQvrbRw2ZTxhg==" + "version": "1.0.30000932", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz", + "integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A==" }, "capture-exit": { "version": "1.2.0", @@ -5078,6 +4923,16 @@ "restore-cursor": "^2.0.0" } }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -5101,16 +4956,6 @@ "string-width": "^2.1.1", "strip-ansi": "^4.0.0", "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } } }, "clone": { @@ -5232,19 +5077,32 @@ } }, "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" }, "columnify": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", - "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", - "dev": true, + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.1.tgz", + "integrity": "sha1-Ff3agDo4dfh/nTArO8goky1mQAM=", "requires": { - "strip-ansi": "^3.0.0", + "strip-ansi": "^2.0.1", "wcwidth": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", + "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=" + }, + "strip-ansi": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", + "integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=", + "requires": { + "ansi-regex": "^1.0.0" + } + } } }, "combined-stream": { @@ -5359,135 +5217,12 @@ "yargs": "^12.0.1" }, "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "has-flag": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5523,36 +5258,6 @@ "requires": { "has-flag": "^2.0.0" } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -5678,6 +5383,15 @@ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "dev": true }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5690,6 +5404,40 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5774,6 +5522,15 @@ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "dev": true }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5786,6 +5543,16 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "meow": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", @@ -5803,6 +5570,30 @@ "trim-newlines": "^2.0.0" } }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5882,6 +5673,15 @@ "trim-off-newlines": "^1.0.0" }, "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5894,6 +5694,16 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "meow": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", @@ -5911,6 +5721,30 @@ "trim-newlines": "^2.0.0" } }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -5981,6 +5815,15 @@ "q": "^1.5.1" }, "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -5993,6 +5836,16 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "meow": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", @@ -6010,6 +5863,30 @@ "trim-newlines": "^2.0.0" } }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -6132,6 +6009,21 @@ "requires": { "is-extglob": "^2.1.1" } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true } } }, @@ -6370,9 +6262,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -6530,9 +6422,9 @@ "integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA==" }, "d3-scale": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.1.2.tgz", - "integrity": "sha512-bESpd64ylaKzCDzvULcmHKZTlzA/6DGSVwx7QSDj/EnX9cpSevsdiwdHFYI9ouo9tNBbV3v5xztHS2uFeOzh8Q==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", "requires": { "d3-array": "^1.2.0", "d3-collection": "1", @@ -6559,14 +6451,14 @@ } }, "d3-selection": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.2.tgz", - "integrity": "sha512-OoXdv1nZ7h2aKMVg3kaUFbLLK5jXUFAMLD/Tu5JA96mjf8f2a9ZUESGY+C36t8R1WFeWk/e55hy54Ml2I62CRQ==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==" }, "d3-shape": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz", - "integrity": "sha512-hUGEozlKecFZ2bOSNt7ENex+4Tk9uc/m0TtTEHBvitCBxUNjhzm5hS2GrrVRD/ae4IylSmxGeqX5tWC2rASMlQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.3.tgz", + "integrity": "sha512-f7V9wHQCmv4s4N7EmD5i0mwJ5y09L8r1rWVrzH1Av0YfgBKJCnTJGho76rS4HNUIw6tTBbWfFcs4ntP/MKWF4A==", "requires": { "d3-path": "1" } @@ -6979,9 +6871,9 @@ } }, "dir-glob": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.1.tgz", - "integrity": "sha512-UN6X6XwRjllabfRhBdkVSo63uurJ8nSvMGrwl94EYVz6g+exhTV+yVSYk5VC/xl3MBFBTtC0J20uFKce4Brrng==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", "dev": true, "requires": { "path-type": "^3.0.0" @@ -7059,6 +6951,12 @@ "yargs": "^7.0.2" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -7095,6 +6993,12 @@ "wrap-ansi": "^2.0.0" } }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -7104,6 +7008,15 @@ "number-is-nan": "^1.0.0" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -7124,6 +7037,15 @@ "strip-ansi": "^3.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -7136,6 +7058,12 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, "yargs": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", @@ -7389,9 +7317,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.102", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.102.tgz", - "integrity": "sha512-2nzZuXw/KBPnI3QX3UOCSRvJiVy7o9+VHRDQ3D/EHCvVc89X6aj/GlNmEgiR2GBIhmSWXIi4W1M5okA5ScSlNg==" + "version": "1.3.109", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.109.tgz", + "integrity": "sha512-1qhgVZD9KIULMyeBkbjU/dWmm30zpPUfdWZfVO3nPhbtqMHJqHr4Ua5wBcWtAymVFrUCuAJxjMF1OhG+bR21Ow==" }, "elliptic": { "version": "6.4.1", @@ -7415,36 +7343,12 @@ "requires": { "columnify": "1.5.1", "lodash.find": "3.2.1" - }, - "dependencies": { - "ansi-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", - "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=" - }, - "columnify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.1.tgz", - "integrity": "sha1-Ff3agDo4dfh/nTArO8goky1mQAM=", - "requires": { - "strip-ansi": "^2.0.1", - "wcwidth": "^1.0.0" - } - }, - "strip-ansi": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", - "integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=", - "requires": { - "ansi-regex": "^1.0.0" - } - } } }, "emoji-regex": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, "emojis-list": { @@ -7471,7 +7375,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -7519,26 +7422,27 @@ } }, "enzyme-adapter-react-16": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.7.1.tgz", - "integrity": "sha512-OQXKgfHWyHN3sFu2nKj3mhgRcqIPIJX6aOzq5AHVFES4R9Dw/vCBZFMPyaG81g2AZ5DogVh39P3MMNUbqNLTcw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.8.0.tgz", + "integrity": "sha512-7cVHIKutqnesGeM3CjNFHSvktpypSWBokrBO8wIW+BVx+HGxWCF87W9TpkIIYJqgCtdw9FQGFrAbLg8kSwPRuQ==", "requires": { - "enzyme-adapter-utils": "^1.9.0", + "enzyme-adapter-utils": "^1.10.0", "function.prototype.name": "^1.1.0", "object.assign": "^4.1.0", - "object.values": "^1.0.4", + "object.values": "^1.1.0", "prop-types": "^15.6.2", - "react-is": "^16.6.1", + "react-is": "^16.7.0", "react-test-renderer": "^16.0.0-0" } }, "enzyme-adapter-utils": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.9.1.tgz", - "integrity": "sha512-LWc88BbKztLXlpRf5Ba/pSMJRaNezAwZBvis3N/IuB65ltZEh2E2obWU9B36pAbw7rORYeBUuqc79OL17ZzN1A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.10.0.tgz", + "integrity": "sha512-VnIXJDYVTzKGbdW+lgK8MQmYHJquTQZiGzu/AseCZ7eHtOMAj4Rtvk8ZRopodkfPves0EXaHkXBDkVhPa3t0jA==", "requires": { "function.prototype.name": "^1.1.0", "object.assign": "^4.1.0", + "object.fromentries": "^2.0.0", "prop-types": "^15.6.2", "semver": "^5.6.0" } @@ -7826,23 +7730,6 @@ "is-fullwidth-code-point": "^2.0.0" } }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } - } - }, "table": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/table/-/table-5.2.2.tgz", @@ -7896,14 +7783,6 @@ "emoji-regex": "^7.0.2", "has": "^1.0.3", "jsx-ast-utils": "^2.0.1" - }, - "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - } } }, "eslint-plugin-react": { @@ -8003,9 +7882,9 @@ "dev": true }, "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, "evp_bytestokey": { @@ -8027,17 +7906,31 @@ } }, "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } } }, "execall": { @@ -8651,49 +8544,6 @@ "pkg-dir": "^3.0.0" }, "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -8717,11 +8567,11 @@ "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=" }, "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { - "locate-path": "^2.0.0" + "locate-path": "^3.0.0" } }, "findup": { @@ -9005,9 +8855,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", "optional": true, "requires": { "nan": "^2.9.2", @@ -9030,7 +8880,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "optional": true, "requires": { @@ -9053,7 +8903,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "optional": true }, @@ -9086,7 +8936,7 @@ } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "optional": true }, @@ -9129,7 +8979,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "optional": true, "requires": { @@ -9147,11 +8997,11 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -9208,16 +9058,16 @@ "optional": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "optional": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "optional": true, "requires": { @@ -9238,7 +9088,7 @@ "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.2.4", "bundled": true, "optional": true, "requires": { @@ -9248,17 +9098,17 @@ } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.10.3", "bundled": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -9274,12 +9124,12 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.5", "bundled": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.2.0", "bundled": true, "optional": true, "requires": { @@ -9346,11 +9196,11 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -9378,15 +9228,15 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "optional": true }, @@ -9401,7 +9251,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.6.0", "bundled": true, "optional": true }, @@ -9447,16 +9297,16 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -9466,11 +9316,11 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -9479,7 +9329,7 @@ "optional": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "optional": true } @@ -9533,6 +9383,12 @@ "wide-align": "^1.1.0" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -9552,6 +9408,15 @@ "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } } } }, @@ -9693,9 +9558,12 @@ "dev": true }, "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } }, "get-value": { "version": "2.0.6", @@ -9751,6 +9619,15 @@ "through2": "^2.0.0" }, "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9763,6 +9640,16 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "meow": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", @@ -9780,6 +9667,30 @@ "trim-newlines": "^2.0.0" } }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -9854,6 +9765,15 @@ "semver": "^5.5.0" }, "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9866,6 +9786,16 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "meow": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", @@ -9883,6 +9813,30 @@ "trim-newlines": "^2.0.0" } }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -10116,6 +10070,14 @@ "timed-out": "^4.0.0", "unzip-response": "^2.0.1", "url-parse-lax": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } } }, "graceful-fs": { @@ -10264,6 +10226,14 @@ "grunt-legacy-log-utils": "~2.0.0", "hooker": "~0.2.3", "lodash": "~4.17.5" + }, + "dependencies": { + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + } } }, "grunt-legacy-log-utils": { @@ -10684,58 +10654,12 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", "dev": true }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "is-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", @@ -10745,40 +10669,6 @@ "ci-info": "^2.0.0" } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -10847,9 +10737,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -11042,16 +10932,6 @@ "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } } }, "interpolate-components": { @@ -11079,9 +10959,9 @@ } }, "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "ip": { "version": "1.1.5", @@ -11618,6 +11498,43 @@ "jest-cli": "^23.6.0" }, "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "jest-cli": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.6.0.tgz", @@ -11703,6 +11620,62 @@ "source-map": "^0.6.0" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -11716,12 +11689,36 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "requires": { - "ansi-regex": "^3.0.0" + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "requires": { + "camelcase": "^4.1.0" } } } @@ -12203,6 +12200,11 @@ "source-map": "^0.5.7" } }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -12211,6 +12213,38 @@ "ms": "2.0.0" } }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "jest-message-util": { "version": "23.4.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-23.4.0.tgz", @@ -12250,15 +12284,103 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "requires": { + "camelcase": "^4.1.0" + } } } }, @@ -12349,9 +12471,9 @@ } }, "js-base64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.0.tgz", - "integrity": "sha512-wlEBIZ5LP8usDylWbDNhKPEFVFdI5hCHpnVoT/Ysvoi/PRhJENm/Rlh9TvjYB38HFfKZN7OzEbRjmjvLkFw11g==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "dev": true }, "js-levenshtein": { @@ -12527,11 +12649,11 @@ } }, "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "requires": { - "invert-kv": "^1.0.0" + "invert-kv": "^2.0.0" } }, "left-pad": { @@ -12623,15 +12745,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } } } }, @@ -12644,51 +12757,6 @@ "figgy-pudding": "^3.5.1", "find-up": "^3.0.0", "ini": "^1.3.5" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - } } }, "libnpmhook": { @@ -12708,15 +12776,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } } } }, @@ -12737,15 +12796,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } } } }, @@ -12772,15 +12822,6 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "ssri": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", @@ -12801,17 +12842,6 @@ "figgy-pudding": "^3.5.1", "get-stream": "^4.0.0", "npm-registry-fetch": "^3.8.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } } }, "libnpmteam": { @@ -12831,15 +12861,6 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } } } }, @@ -12957,9 +12978,9 @@ } }, "loader-runner": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz", - "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", "dev": true }, "loader-utils": { @@ -12985,11 +13006,11 @@ } }, "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "requires": { - "p-locate": "^2.0.0", + "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, @@ -13345,12 +13366,6 @@ "figgy-pudding": "^3.5.1" } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", @@ -13371,7 +13386,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, "requires": { "p-defer": "^1.0.0" } @@ -13412,9 +13426,9 @@ "integrity": "sha512-HduzIW2xApSXKXJSpCipSxKyvMbwRRa/TwMbepmlZziKdH8548WSoDP4SxzulEKjlo8BE39l+2fwJZuRKOln6g==" }, "math-random": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.2.tgz", - "integrity": "sha512-Bp2Bx2wFaUymE7pWi0bbldiheIXMvyzC3hRkT5YAv2qiqqJO5VB8KafgYgZmGCxkTmloLuAx3Jv2OmJ66990mg==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" }, "mathml-tag-names": { "version": "2.1.0", @@ -13449,11 +13463,13 @@ "dev": true }, "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", + "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", "requires": { - "mimic-fn": "^1.0.0" + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^1.0.0", + "p-is-promise": "^2.0.0" } }, "memize": { @@ -13513,6 +13529,19 @@ "yargs-parser": "^10.0.0" }, "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -13524,6 +13553,36 @@ "strip-bom": "^3.0.0" } }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -13838,9 +13897,9 @@ "dev": true }, "moment": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz", - "integrity": "sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==" + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "moment-timezone": { "version": "0.5.23", @@ -13968,8 +14027,7 @@ "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-addon-api": { "version": "1.6.2", @@ -14040,9 +14098,9 @@ "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" }, "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", "dev": true, "requires": { "assert": "^1.1.1", @@ -14052,7 +14110,7 @@ "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", - "events": "^1.0.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.0", @@ -14066,7 +14124,7 @@ "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", - "util": "^0.10.3", + "util": "^0.11.0", "vm-browserify": "0.0.4" }, "dependencies": { @@ -14116,9 +14174,9 @@ } }, "node-releases": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.3.tgz", - "integrity": "sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.6.tgz", + "integrity": "sha512-lODUVHEIZutZx+TDdOk47qLik8FJMXzJ+WnyUGci1MTvTOyzZrz5eVPIIpc5Hb3NfHZGeGHeuwrRYVI1PEITWg==", "requires": { "semver": "^5.3.0" } @@ -14150,6 +14208,12 @@ "true-case-path": "^1.0.2" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -14238,6 +14302,15 @@ "strip-indent": "^1.0.1" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", @@ -14305,9 +14378,9 @@ } }, "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.2.tgz", + "integrity": "sha512-YcMnjqeoUckXTPKZSAsPjUPLxH85XotbpqK3w4RyCwdFQSU5FxxBys8buehkSfg0j9fKvV1hn7O0+8reEgkAiw==", "requires": { "hosted-git-info": "^2.1.4", "is-builtin-module": "^1.0.0", @@ -14584,7 +14657,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", "integrity": "sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==", - "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.11.0", @@ -14681,6 +14753,12 @@ "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", "dev": true }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -14741,6 +14819,15 @@ "pinkie-promise": "^2.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -14816,13 +14903,13 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, "os-tmpdir": { @@ -14854,8 +14941,7 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-finally": { "version": "1.0.0", @@ -14863,25 +14949,24 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, "p-is-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", + "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==" }, "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", + "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", "requires": { - "p-try": "^1.0.0" + "p-try": "^2.0.0" } }, "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { - "p-limit": "^1.1.0" + "p-limit": "^2.0.0" } }, "p-map": { @@ -14912,9 +14997,9 @@ "dev": true }, "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" }, "p-waterfall": { "version": "1.0.0", @@ -14994,15 +15079,6 @@ "y18n": "^4.0.0" } }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -15054,12 +15130,6 @@ "yallist": "^3.0.2" } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", @@ -15069,9 +15139,9 @@ } }, "pako": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.7.tgz", - "integrity": "sha512-3HNK5tW4x8o5mO8RuHZp3Ydw9icZXx0RANAOMzlMzx7LVXhMJ4mo3MOBpzyd7r/+RUu8BmndP47LXT+vzjtWcQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", "dev": true }, "parallel-transform": { @@ -15135,16 +15205,17 @@ "dev": true }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.3.tgz", + "integrity": "sha512-VrPoetlz7B/FqjBLD2f5wBVZvsZVLnRUrxVLfRYhGXCODa/NWE4p3Wp+6+aV3ZPL3KM7/OZmxDIwwijD7yuucg==", "dev": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-entities": { @@ -15330,6 +15401,46 @@ "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", "requires": { "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + } } }, "please-upgrade-node": { @@ -15423,6 +15534,12 @@ "postcss": "^5.2.16" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -15468,6 +15585,15 @@ "supports-color": "^3.2.3" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", @@ -15526,9 +15652,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15585,9 +15711,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15624,9 +15750,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15662,9 +15788,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15700,9 +15826,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15740,9 +15866,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15783,9 +15909,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15821,9 +15947,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15976,6 +16102,12 @@ "integrity": "sha512-ribEzWEhWKKjY+1FdKCryo+HiN/1idPjUB8vyR5Yf221MtGzCd5+7OwPvWvYHerHHC2eJLr6MhvumbTocXGY7Q==", "dev": true }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, "chalk": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", @@ -16005,6 +16137,18 @@ "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", "dev": true }, + "emoji-regex": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", + "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, "globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", @@ -16063,6 +16207,15 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, "parse-json": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-3.0.0.tgz", @@ -16247,7 +16400,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -16449,9 +16601,9 @@ } }, "react-dates": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-18.3.1.tgz", - "integrity": "sha512-sJdEt1KCHgHED8vRqxkzggF9XyFv58A9+xA0oSATKemBXroe/KVN6svp3TcKZ2VWMbdPmeM5Bd3xRWTkrGk9Dg==", + "version": "18.4.1", + "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-18.4.1.tgz", + "integrity": "sha512-ew6HiORfbJkEGlJ+5SMC5GtgI87zj2BqNv8tRsdnPtgLMt5fY2Z9dUFxc+XATeRHs+wOm4ku0dlKWpuqBzYapQ==", "requires": { "airbnb-prop-types": "^2.10.0", "consolidated-events": "^1.1.1 || ^2.0.0", @@ -16670,9 +16822,9 @@ } }, "react-transition-group": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.2.tgz", - "integrity": "sha512-vwHP++S+f6KL7rg8V1mfs62+MBKtbMeZDR8KiNmD7v98Gs3UPGsDZDahPJH2PVprFW5YHJfh6cbNim3zPndaSQ==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz", + "integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==", "requires": { "dom-helpers": "^3.3.1", "loose-envify": "^1.4.0", @@ -17251,6 +17403,16 @@ "safe-regex": "^1.1.0" } }, + "regexp-tree": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.1.tgz", + "integrity": "sha512-HwRjOquc9QOwKTgbxvZTcddS5mlNlwePMQ3NFL8broajMLD5CXDAqas8Y5yxJH5QtZp5iRor3YCILd5pz71Cgw==", + "requires": { + "cli-table3": "^0.5.0", + "colors": "^1.1.2", + "yargs": "^12.0.5" + } + }, "regexpp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", @@ -17438,164 +17600,11 @@ "yargs": "12.0.5" }, "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - }, "colors": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.4.tgz", "integrity": "sha512-6Y+iBnWmXL+AWtlOp2Vr6R2w5MUlNJRwR0ShVFaAb1CqWzhPOpQg4L0jxD+xpw/Nc8QJwaq3KM79QUCriY8CWQ==", "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -17703,9 +17712,9 @@ "dev": true }, "resolve": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", - "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", "requires": { "path-parse": "^1.0.6" } @@ -17878,9 +17887,9 @@ } }, "rxjs": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", - "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -18195,6 +18204,12 @@ "yargs": "^7.0.0" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", @@ -18212,6 +18227,12 @@ "wrap-ansi": "^2.0.0" } }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -18221,6 +18242,15 @@ "number-is-nan": "^1.0.0" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -18241,12 +18271,27 @@ "strip-ansi": "^3.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, "yargs": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", @@ -18327,9 +18372,9 @@ }, "dependencies": { "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.3.0.tgz", + "integrity": "sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==", "dev": true } } @@ -18819,9 +18864,9 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" }, "sshpk": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz", - "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -18920,9 +18965,9 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { "inherits": "~2.0.1", @@ -19017,16 +19062,6 @@ "requires": { "astral-regex": "^1.0.0", "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } } }, "string-width": { @@ -19036,16 +19071,6 @@ "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } } }, "string.prototype.trim": { @@ -19085,18 +19110,11 @@ "dev": true }, "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - } + "ansi-regex": "^3.0.0" } }, "strip-bom": { @@ -19458,9 +19476,9 @@ } }, "ignore": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.4.tgz", - "integrity": "sha512-WLsTMEhsQuXpCiG173+f3aymI43SXa+fB1rSfbzyP4GkPP+ZFVuO0/3sFUGNBtifisPeDcl/uD/Y2NxZ7xFq4g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.0.5.tgz", + "integrity": "sha512-kOC8IUb8HSDMVcYrDVezCxpJkzSQWTAzf3olpKM6o9rM5zpojx23O0Fl8Wr4+qJ6ZbPEHqf1fdwev/DS7v7pmA==", "dev": true }, "import-lazy": { @@ -19581,9 +19599,9 @@ "dev": true }, "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -19592,12 +19610,12 @@ } }, "postcss-less": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.0.tgz", - "integrity": "sha512-+fDH2A9zV8B4gFu3Idhq8ma09/mMBXXc03T2lL9CHjBQqKrfUit+TrQrnojc6Y4k7N4E+tyE1Uj5U1tcoKtXLQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.2.tgz", + "integrity": "sha512-66ZBVo1JGkQ7r13M97xcHcyarWpgg21RaqIZWZXHE3XOtb5+ywK1uZWeY1DYkYRkIX/l8Hvxnx9iSKB68nFr+w==", "dev": true, "requires": { - "postcss": "^7.0.3" + "postcss": "^7.0.14" } }, "postcss-scss": { @@ -19633,9 +19651,9 @@ "dev": true }, "slice-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.0.0.tgz", - "integrity": "sha512-4j2WTWjp3GsZ+AOagyzVbzp4vWGtZ0hEZ/gDY/uTvm6MTxUfTUIsnMIFb1bn8o0RuXiqUw15H1bue8f22Vw2oQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", "dev": true, "requires": { "ansi-styles": "^3.2.0", @@ -19659,14 +19677,14 @@ } }, "table": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/table/-/table-5.2.0.tgz", - "integrity": "sha512-hAdBBAMCZl4/U3eQhsPN2Z8wRJC98lpRhDW2I86VQbPBqyj4E681VhvUkfb90qUJ4rnRfu8t4/8SGHPsAH1ygg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/table/-/table-5.2.2.tgz", + "integrity": "sha512-f8mJmuu9beQEDkKHLzOv4VxVYlU68NpdzjbGPl69i4Hx0sTopJuNxuzJd17iV2h24dAfa93u794OnDA5jqXvfQ==", "dev": true, "requires": { "ajv": "^6.6.1", "lodash": "^4.17.11", - "slice-ansi": "2.0.0", + "slice-ansi": "^2.0.0", "string-width": "^2.1.1" } } @@ -19699,9 +19717,9 @@ } }, "stylelint-scss": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.0.tgz", - "integrity": "sha512-i8YhQd7IWWEacjUGvZycQ16c9NBj+Dbir1UwfQMlK1+5hr7crtxoo4j8TS8RR5ILP6g3bHSn24a8AvKrtFDBlQ==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.5.1.tgz", + "integrity": "sha512-XNWKTU1a2EUNWdauxHPTJlGNNQzIbg48OTTIdBs5xTXxpbYAGtX/J+jBqMPjxfdySXijc/mexubuZ+ZinUGGgw==", "dev": true, "requires": { "lodash": "^4.17.11", @@ -19740,9 +19758,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.11.tgz", - "integrity": "sha512-9AXb//5UcjeOEof9T+yPw3XTa5SL207ZOIC/lHYP4mbUTEh4M0rDAQekQpVANCZdwQwKhBtFZCk3i3h3h2hdWg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -19828,11 +19846,11 @@ } }, "tannin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.0.1.tgz", - "integrity": "sha512-dDtnwHQ63bS/Gz0ZLY+E+JCdRoTZkmoKDoC64y3hzAD2X2qrp8jSuWNUjtiYHA48mtj4Ens9xl4knAOm1t+rfQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.0.2.tgz", + "integrity": "sha512-5pqwELzsIO+zO3V3/jjlMR/ykzK41YNFLvb4SwPeAgrTiCjXwL/P3Om5uOiJk/iKdq6lBIO94rYAfBsEHu2vlA==", "requires": { - "@tannin/plural-forms": "^1.0.0" + "@tannin/plural-forms": "^1.0.2" } }, "tapable": { @@ -19887,6 +19905,29 @@ "dev": true, "requires": { "execa": "^0.7.0" + }, + "dependencies": { + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + } } }, "terser": { @@ -20004,12 +20045,6 @@ "figgy-pudding": "^3.5.1" } }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", @@ -20113,9 +20148,9 @@ "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=" }, "tinydate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.0.0.tgz", - "integrity": "sha1-IPMXVqE5We+MV+wTO6KbWt4ELKw=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.0.1.tgz", + "integrity": "sha512-Imqa6iv3Ig5FmC3ESwmqczusIn1h8D5RqNbpatGc1eLHeoytuhodbsAPpSJ8iKiLhxBtLuRsrywWHlJM1bA3Rg==", "dev": true }, "tmp": { @@ -20678,9 +20713,9 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" @@ -20736,9 +20771,9 @@ } }, "validator": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.10.0.tgz", - "integrity": "sha512-DyZyLJlMXM3CGdVaVHE/EDzCagMRoPI3mmGdxxNQbqkGqh56+M3d1i0ZAWd69En8U21DHbPTn12aOdhO+hfm5w==" + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" }, "value-equal": { "version": "0.4.0", @@ -20886,9 +20921,9 @@ }, "dependencies": { "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.3.0.tgz", + "integrity": "sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==", "dev": true }, "arr-diff": { @@ -21220,12 +21255,6 @@ "yargs": "^12.0.2" }, "dependencies": { - "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", - "dev": true - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -21239,39 +21268,6 @@ "which": "^1.2.9" } }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -21282,77 +21278,6 @@ "resolve-cwd": "^2.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -21361,36 +21286,6 @@ "requires": { "find-up": "^3.0.0" } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -21494,6 +21389,11 @@ "strip-ansi": "^3.0.1" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -21511,6 +21411,14 @@ "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } } } }, @@ -21528,9 +21436,9 @@ } }, "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", + "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -21613,9 +21521,9 @@ "dev": true }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { "version": "2.1.2", @@ -21633,6 +21541,12 @@ "parent-require": "^1.0.0" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -21652,6 +21566,15 @@ "supports-color": "^2.0.0" } }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", @@ -21661,30 +21584,31 @@ } }, "yargs": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", - "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", "requires": { "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", + "os-locale": "^3.0.0", "require-directory": "^2.1.1", "require-main-filename": "^1.0.1", "set-blocking": "^2.0.0", "string-width": "^2.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" } }, "yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } From 49e78b90cf4391751949ce2abf3fbffd14c524b8 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Thu, 31 Jan 2019 09:04:11 +0800 Subject: [PATCH 81/91] Add settings page with excluded order statuses (https://github.com/woocommerce/woocommerce-admin/pull/1364) * Add settings page routes * Add control options for excluded statuses * Add control options for excluded statuses * Add excluded order statuses to rest api * Add wc settings to wc-api * Add wc settings to wc-api * Split and validate multiselect values in settings controller * Add wcAdminSettings to wcSettings global * Set initial excluded statuses from serverside wcSettings data * Add extensible filter for wcSettings global * Split arrays into comma separated strings in wc-api * Extract setting as separate component * Extra settings to config file * Add checkboxGroup option as input type * Separate status types into default and custom groups * Add setting option styling * Add responsive styling for settings * Fix wpClosedMenu and wpOpenMenu for settings page * Add support for resetting to default values * Only show checkbox group if options are available * Add proptypes to Setting component * Add extensible filter to analytics settings * Add readme for settings config and extensibility * Hook up excluded status settings to reports * Pass object to settings API instead of comma delimited string * Fix inpuType -> inputType typo * Remove hasError from constructor * Bump settings API to v4 * Use interpolateComponents instead of dangerously setting html * Use empty array in initial excldued statuses setting value if none is retrieved * Remove double check for refunded status in default order statuses * Update settings wc-api to use namespace * Add aria=labelledby to checkbox group --- .../client/analytics/settings/README.md | 34 +++++ .../client/analytics/settings/config.js | 68 +++++++++ .../client/analytics/settings/index.js | 134 +++++++++++++++++ .../client/analytics/settings/index.scss | 17 +++ .../client/analytics/settings/setting.js | 141 ++++++++++++++++++ .../client/analytics/settings/setting.scss | 49 ++++++ .../client/layout/controller.js | 7 + .../client/wc-api/settings/index.js | 13 ++ .../client/wc-api/settings/mutations.js | 10 ++ .../client/wc-api/settings/operations.js | 71 +++++++++ .../client/wc-api/settings/selectors.js | 14 ++ .../client/wc-api/wc-api-spec.js | 9 +- ...-admin-rest-setting-options-controller.php | 27 ++++ .../includes/class-wc-admin-api-init.php | 16 ++ .../class-wc-admin-reports-data-store.php | 4 +- plugins/woocommerce-admin/lib/admin.php | 44 ++++++ .../woocommerce-admin/lib/client-assets.php | 20 +++ 17 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce-admin/client/analytics/settings/README.md create mode 100644 plugins/woocommerce-admin/client/analytics/settings/config.js create mode 100644 plugins/woocommerce-admin/client/analytics/settings/index.js create mode 100644 plugins/woocommerce-admin/client/analytics/settings/index.scss create mode 100644 plugins/woocommerce-admin/client/analytics/settings/setting.js create mode 100644 plugins/woocommerce-admin/client/analytics/settings/setting.scss create mode 100644 plugins/woocommerce-admin/client/wc-api/settings/index.js create mode 100644 plugins/woocommerce-admin/client/wc-api/settings/mutations.js create mode 100644 plugins/woocommerce-admin/client/wc-api/settings/operations.js create mode 100644 plugins/woocommerce-admin/client/wc-api/settings/selectors.js create mode 100644 plugins/woocommerce-admin/includes/api/class-wc-admin-rest-setting-options-controller.php diff --git a/plugins/woocommerce-admin/client/analytics/settings/README.md b/plugins/woocommerce-admin/client/analytics/settings/README.md new file mode 100644 index 00000000000..fa8f5ffaa0d --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/settings/README.md @@ -0,0 +1,34 @@ +Settings +======= + +The settings used to modify the way data is retreived or displayed in WooCommerce reports. + +## Extending Settings + +Settings can be added, removed, or modified outside oc `wc-admin` by hooking into `woocommerce_admin_analytics_settings`. For example: + +```js +addFilter( 'woocommerce_admin_analytics_settings', 'wc-example/my-setting', settings => { + return [ + ...settings, + { + name: 'custom_setting', + label: __( 'Custom setting:', 'wc-admin' ), + inputType: 'text', + helpText: __( 'Help text to describe what the setting does.' ), + initialValue: 'Initial value used', + defaultValue: 'Default value', + }, + ]; +} ); +``` + +Each settings has the following properties: + +- `name` (string): The slug of the setting to be updated. +- `label` (string): The label used to describe and displayed next to the setting. +- `inputType` (enum: text|checkbox|checkboxGroup): The type of input to use. +- `helpText` (string): Text displayed beneath the setting. +- `options` (array): Array of options used for inputs with selectable options. +- `initialValue` (string|array): Initial value used when rendering the setting. +- `defaultValue` (string|array): Value used when resetting to default settings. diff --git a/plugins/woocommerce-admin/client/analytics/settings/config.js b/plugins/woocommerce-admin/client/analytics/settings/config.js new file mode 100644 index 00000000000..282108f82f9 --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/settings/config.js @@ -0,0 +1,68 @@ +/** @format */ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { applyFilters } from '@wordpress/hooks'; +import interpolateComponents from 'interpolate-components'; + +/** + * WooCommerce dependencies + */ +import { Link } from '@woocommerce/components'; + +const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings'; + +const defaultOrderStatuses = [ + 'completed', + 'processing', + 'refunded', + 'cancelled', + 'failed', + 'pending', + 'on-hold', +]; +const orderStatuses = Object.keys( wcSettings.orderStatuses ) + .filter( status => status !== 'refunded' ) + .map( key => { + return { + value: key, + label: wcSettings.orderStatuses[ key ], + description: sprintf( + __( 'Exclude the %s status from reports', 'wc-admin' ), + wcSettings.orderStatuses[ key ] + ), + }; + } ); + +export const analyticsSettings = applyFilters( SETTINGS_FILTER, [ + { + name: 'woocommerce_excluded_report_order_statuses', + label: __( 'Excluded Statuses:', 'wc-admin' ), + inputType: 'checkboxGroup', + options: [ + { + key: 'defaultStatuses', + options: orderStatuses.filter( status => defaultOrderStatuses.includes( status.value ) ), + }, + { + key: 'customStatuses', + label: __( 'Custom Statuses', 'wc-admin' ), + options: orderStatuses.filter( status => ! defaultOrderStatuses.includes( status.value ) ), + }, + ], + helpText: interpolateComponents( { + mixedString: __( + 'Orders with these statuses are excluded from the totals in your reports. ' + + 'The {{strong}}Refunded{{/strong}} status can not be excluded. {{moreLink}}Learn more{{/moreLink}}', + 'wc-admin' + ), + components: { + strong: , + moreLink: , // @TODO: this needs to be replaced with a real link. + }, + } ), + initialValue: wcSettings.wcAdminSettings.woocommerce_excluded_report_order_statuses || [], + defaultValue: [ 'pending', 'cancelled', 'failed' ], + }, +] ); diff --git a/plugins/woocommerce-admin/client/analytics/settings/index.js b/plugins/woocommerce-admin/client/analytics/settings/index.js new file mode 100644 index 00000000000..2b0c1faac80 --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/settings/index.js @@ -0,0 +1,134 @@ +/** @format */ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; +import { remove } from 'lodash'; +import { withDispatch } from '@wordpress/data'; + +/** + * WooCommerce dependencies + */ +import { SectionHeader, useFilters } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import './index.scss'; +import { analyticsSettings } from './config'; +import Header from 'header'; +import Setting from './setting'; + +const SETTINGS_FILTER = 'woocommerce_admin_analytics_settings'; + +class Settings extends Component { + constructor() { + super( ...arguments ); + + const settings = {}; + analyticsSettings.forEach( setting => ( settings[ setting.name ] = setting.initialValue ) ); + + this.state = { + settings: settings, + }; + + this.handleInputChange = this.handleInputChange.bind( this ); + } + + componentDidCatch( error ) { + this.setState( { + hasError: true, + } ); + /* eslint-disable no-console */ + console.warn( error ); + /* eslint-enable no-console */ + } + + resetDefaults = () => { + if ( + window.confirm( + __( 'Are you sure you want to reset all settings to default values?', 'wc-admin' ) + ) + ) { + const settings = {}; + analyticsSettings.forEach( setting => ( settings[ setting.name ] = setting.defaultValue ) ); + this.setState( { settings }, this.saveChanges ); + } + }; + + saveChanges = () => { + this.props.updateSettings( this.state.settings ); + // @TODO: Need a confirmation on successful update. + }; + + handleInputChange( e ) { + const { checked, name, type, value } = e.target; + const { settings } = this.state; + + if ( 'checkbox' === type ) { + if ( checked ) { + settings[ name ].push( value ); + } else { + remove( settings[ name ], v => v === value ); + } + } else { + settings[ name ] = value; + } + + this.setState( { settings } ); + } + + render() { + const { hasError } = this.state; + if ( hasError ) { + return null; + } + + return ( + +
+ +
+ { analyticsSettings.map( setting => ( + + ) ) } +
+ + +
+
+ + ); + } +} + +export default compose( + withDispatch( dispatch => { + const { updateSettings } = dispatch( 'wc-api' ); + + return { + updateSettings, + }; + } ) +)( useFilters( SETTINGS_FILTER )( Settings ) ); diff --git a/plugins/woocommerce-admin/client/analytics/settings/index.scss b/plugins/woocommerce-admin/client/analytics/settings/index.scss new file mode 100644 index 00000000000..773dbca9c27 --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/settings/index.scss @@ -0,0 +1,17 @@ +/** @format */ + +.woocommerce-settings__wrapper { + @include breakpoint( '>782px' ) { + padding: 0 ($gap - 3); + } +} + +.woocommerce-settings__actions { + @include breakpoint( '>1280px' ) { + margin-left: 15%; + } + + button { + margin-right: $gap; + } +} diff --git a/plugins/woocommerce-admin/client/analytics/settings/setting.js b/plugins/woocommerce-admin/client/analytics/settings/setting.js new file mode 100644 index 00000000000..ef7b96286ab --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/settings/setting.js @@ -0,0 +1,141 @@ +/** @format */ +/** + * External dependencies + */ +import { Component } from '@wordpress/element'; +import PropTypes from 'prop-types'; +import { uniqueId } from 'lodash'; + +/** + * Internal dependencies + */ +import './setting.scss'; + +class Setting extends Component { + renderInput = () => { + const { handleChange, name, inputType, options, value } = this.props; + const id = uniqueId( name ); + + switch ( inputType ) { + case 'checkboxGroup': + return options.map( + optionGroup => + optionGroup.options.length > 0 && ( +
+ { optionGroup.label && ( + + { optionGroup.label } + + ) } + { this.renderCheckboxOptions( optionGroup.options ) } +
+ ) + ); + case 'checkbox': + return this.renderCheckboxOptions( options ); + case 'text': + default: + return ( + + ); + } + }; + + renderCheckboxOptions( options ) { + const { handleChange, name, value } = this.props; + + return options.map( option => { + const id = uniqueId( name + '-' + option.value ); + return ( + + ); + } ); + } + + render() { + const { helpText, label, name } = this.props; + + return ( +
+
+ { label } +
+
+ { this.renderInput() } + { helpText && { helpText } } +
+
+ ); + } +} + +Setting.propTypes = { + /** + * Function assigned to the onChange of all inputs. + */ + handleChange: PropTypes.func.isRequired, + /** + * Optional help text displayed underneath the setting. + */ + helpText: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array ] ), + /** + * Type of input to use; defaults to a text input. + */ + inputType: PropTypes.oneOf( [ 'checkbox', 'checkboxGroup', 'text' ] ), + /** + * Label used for describing the setting. + */ + label: PropTypes.string.isRequired, + /** + * Setting slug applied to input names. + */ + name: PropTypes.string.isRequired, + /** + * Array of options used for when the `inputType` allows multiple selections. + */ + options: PropTypes.arrayOf( + PropTypes.shape( { + /** + * Input value for this option. + */ + value: PropTypes.string, + /** + * Label for this option or above a group for a group `inputType`. + */ + label: PropTypes.string, + /** + * Description used for screen readers. + */ + description: PropTypes.string, + /** + * Key used for a group `inputType`. + */ + key: PropTypes.string, + /** + * Nested options for a group `inputType`. + */ + options: PropTypes.array, + } ) + ), + /** + * The string value used for the input or array of items if the input allows multiselection. + */ + value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array ] ), +}; + +export default Setting; diff --git a/plugins/woocommerce-admin/client/analytics/settings/setting.scss b/plugins/woocommerce-admin/client/analytics/settings/setting.scss new file mode 100644 index 00000000000..98440fdcbfb --- /dev/null +++ b/plugins/woocommerce-admin/client/analytics/settings/setting.scss @@ -0,0 +1,49 @@ +/** @format */ + +.woocommerce-setting { + display: flex; + margin-bottom: $gap-large; + @include breakpoint( '<1280px' ) { + flex-direction: column; + } +} + +.woocommerce-setting__label { + @include font-size(16); + margin-bottom: $gap; + padding-right: $gap; + font-weight: bold; + @include breakpoint( '>1280px' ) { + width: 15%; + } +} + +.woocommerce-setting__options { + display: flex; + flex-direction: column; + @include breakpoint( '>1280px' ) { + width: 35%; + } + + label { + width: 100%; + display: block; + margin-bottom: $gap-small; + color: $core-grey-dark-500; + } + + input[type='checkbox'] { + margin-right: $gap-small; + } +} + +.woocommerce-setting__options-group-label { + display: block; + font-weight: bold; + margin-bottom: $gap-small; +} + +.woocommerce-setting__help { + font-style: italic; + color: $core-grey-dark-300; +} diff --git a/plugins/woocommerce-admin/client/layout/controller.js b/plugins/woocommerce-admin/client/layout/controller.js index 7050d60ff78..d9960efd0a8 100644 --- a/plugins/woocommerce-admin/client/layout/controller.js +++ b/plugins/woocommerce-admin/client/layout/controller.js @@ -16,6 +16,7 @@ import { getPersistedQuery, stringifyQuery } from '@woocommerce/navigation'; */ import Analytics from 'analytics'; import AnalyticsReport from 'analytics/report'; +import AnalyticsSettings from 'analytics/settings'; import Dashboard from 'dashboard'; import DevDocs from 'devdocs'; @@ -33,6 +34,12 @@ const getPages = () => { wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue', wpClosedMenu: 'toplevel_page_woocommerce', }, + { + container: AnalyticsSettings, + path: '/analytics/settings', + wpOpenMenu: 'toplevel_page_wc-admin--analytics-revenue', + wpClosedMenu: 'toplevel_page_woocommerce', + }, { container: AnalyticsReport, path: '/analytics/:report', diff --git a/plugins/woocommerce-admin/client/wc-api/settings/index.js b/plugins/woocommerce-admin/client/wc-api/settings/index.js new file mode 100644 index 00000000000..094ff3a82fa --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/settings/index.js @@ -0,0 +1,13 @@ +/** @format */ +/** + * Internal dependencies + */ +import operations from './operations'; +import selectors from './selectors'; +import mutations from './mutations'; + +export default { + operations, + selectors, + mutations, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/settings/mutations.js b/plugins/woocommerce-admin/client/wc-api/settings/mutations.js new file mode 100644 index 00000000000..f7e2ff21b23 --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/settings/mutations.js @@ -0,0 +1,10 @@ +/** @format */ + +const updateSettings = operations => settingFields => { + const resourceKey = 'settings'; + operations.update( [ resourceKey ], { [ resourceKey ]: settingFields } ); +}; + +export default { + updateSettings, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/settings/operations.js b/plugins/woocommerce-admin/client/wc-api/settings/operations.js new file mode 100644 index 00000000000..ada55a54fb2 --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/settings/operations.js @@ -0,0 +1,71 @@ +/** @format */ + +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { pick } from 'lodash'; + +/** + * Internal dependencies + */ +import { NAMESPACE } from '../constants'; + +function read( resourceNames, fetch = apiFetch ) { + return [ ...readSettings( resourceNames, fetch ) ]; +} + +function update( resourceNames, data, fetch = apiFetch ) { + return [ ...updateSettings( resourceNames, data, fetch ) ]; +} + +function readSettings( resourceNames, fetch ) { + if ( resourceNames.includes( 'settings' ) ) { + const url = NAMESPACE + '/settings/wc_admin'; + + return [ + fetch( { path: url } ) + .then( settingsToSettingsResource ) + .catch( error => { + return { [ 'settings' ]: { error: String( error.message ) } }; + } ), + ]; + } + return []; +} + +function updateSettings( resourceNames, data, fetch ) { + const resourceName = 'settings'; + const settingsFields = [ 'woocommerce_excluded_report_order_statuses' ]; + + if ( resourceNames.includes( resourceName ) ) { + const url = NAMESPACE + '/settings/wc_admin/'; + const settingsData = pick( data[ resourceName ], settingsFields ); + + const promises = Object.keys( settingsData ).map( setting => { + return fetch( { + path: url + setting, + method: 'POST', + data: { value: settingsData[ setting ] }, + } ) + .then( settingsToSettingsResource ) + .catch( error => { + return { [ resourceName ]: { error } }; + } ); + } ); + + return [ promises ]; + } + return []; +} + +function settingsToSettingsResource( settings ) { + const settingsData = {}; + settings.forEach( setting => ( settingsData[ setting.id ] = setting.value ) ); + return { [ 'settings' ]: { data: settingsData } }; +} + +export default { + read, + update, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/settings/selectors.js b/plugins/woocommerce-admin/client/wc-api/settings/selectors.js new file mode 100644 index 00000000000..d586d90958e --- /dev/null +++ b/plugins/woocommerce-admin/client/wc-api/settings/selectors.js @@ -0,0 +1,14 @@ +/** @format */ + +/** + * Internal dependencies + */ +import { DEFAULT_REQUIREMENT } from '../constants'; + +const getSettings = ( getResource, requireResource ) => ( requirement = DEFAULT_REQUIREMENT ) => { + return requireResource( requirement, 'settings' ).data; +}; + +export default { + getSettings, +}; diff --git a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js index df38ee4a4ef..9c2df94cb55 100644 --- a/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js +++ b/plugins/woocommerce-admin/client/wc-api/wc-api-spec.js @@ -9,11 +9,13 @@ import orders from './orders'; import reportItems from './reports/items'; import reportStats from './reports/stats'; import reviews from './reviews'; +import settings from './settings'; import user from './user'; function createWcApiSpec() { return { mutations: { + ...settings.mutations, ...user.mutations, }, selectors: { @@ -23,6 +25,7 @@ function createWcApiSpec() { ...reportItems.selectors, ...reportStats.selectors, ...reviews.selectors, + ...settings.selectors, ...user.selectors, }, operations: { @@ -34,11 +37,15 @@ function createWcApiSpec() { ...reportItems.operations.read( resourceNames ), ...reportStats.operations.read( resourceNames ), ...reviews.operations.read( resourceNames ), + ...settings.operations.read( resourceNames ), ...user.operations.read( resourceNames ), ]; }, update( resourceNames, data ) { - return [ ...user.operations.update( resourceNames, data ) ]; + return [ + ...settings.operations.update( resourceNames, data ), + ...user.operations.update( resourceNames, data ), + ]; }, }, }; diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-setting-options-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-setting-options-controller.php new file mode 100644 index 00000000000..bfa1a2fe6ec --- /dev/null +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-setting-options-controller.php @@ -0,0 +1,27 @@ +[\w-]+)'] ) + && isset( $endpoints['/wc/v4/settings/(?P[\w-]+)'][5] ) + && isset( $endpoints['/wc/v4/settings/(?P[\w-]+)'][4] ) + && isset( $endpoints['/wc/v4/settings/(?P[\w-]+)'][3] ) + && $endpoints['/wc/v4/settings/(?P[\w-]+)'][3]['callback'][0] instanceof WC_Admin_REST_Setting_Options_Controller + && $endpoints['/wc/v4/settings/(?P[\w-]+)'][4]['callback'][0] instanceof WC_Admin_REST_Setting_Options_Controller + && $endpoints['/wc/v4/settings/(?P[\w-]+)'][5]['callback'][0] instanceof WC_Admin_REST_Setting_Options_Controller + ) { + $endpoints['/wc/v4/settings/(?P[\w-]+)'][0] = $endpoints['/wc/v4/settings/(?P[\w-]+)'][3]; + $endpoints['/wc/v4/settings/(?P[\w-]+)'][1] = $endpoints['/wc/v4/settings/(?P[\w-]+)'][4]; + $endpoints['/wc/v4/settings/(?P[\w-]+)'][2] = $endpoints['/wc/v4/settings/(?P[\w-]+)'][5]; + } + return $endpoints; } 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 c884cae5826..a8b5a5d4b79 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 @@ -406,7 +406,9 @@ class WC_Admin_Reports_Data_Store { * @return array */ protected static function get_excluded_report_order_statuses() { - return apply_filters( 'woocommerce_reports_excluded_order_statuses', array( 'refunded', 'pending', 'failed', 'cancelled' ) ); + $excluded_statuses = WC_Admin_Settings::get_option( 'woocommerce_excluded_report_order_statuses', array( 'pending', 'failed', 'cancelled' ) ); + $excluded_statuses[] = 'refunded'; + return apply_filters( 'woocommerce_reports_excluded_order_statuses', $excluded_statuses ); } /** diff --git a/plugins/woocommerce-admin/lib/admin.php b/plugins/woocommerce-admin/lib/admin.php index bfc132c9295..6d5bd9f0483 100644 --- a/plugins/woocommerce-admin/lib/admin.php +++ b/plugins/woocommerce-admin/lib/admin.php @@ -157,6 +157,14 @@ function wc_admin_register_pages() { ) ); + wc_admin_register_page( + array( + 'title' => __( 'Settings', 'wc-admin' ), + 'parent' => '/analytics/revenue', + 'path' => '/analytics/settings', + ) + ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { wc_admin_register_page( array( @@ -447,3 +455,39 @@ function wc_admin_update_user_data_values( $values, $user, $field_id ) { return $updates; } + +/** + * Register the admin settings for use in the WC REST API + * + * @param array $groups Array of setting groups. + * @return array + */ +function wc_admin_add_settings_group( $groups ) { + $groups[] = array( + 'id' => 'wc_admin', + 'label' => __( 'WooCommerce Admin', 'wc-admin' ), + 'description' => __( 'Settings for WooCommerce admin reporting.', 'wc-admin' ), + ); + return $groups; +} +add_filter( 'woocommerce_settings_groups', 'wc_admin_add_settings_group' ); + +/** + * Add WC Admin specific settings + * + * @param array $settings Array of settings in wc admin group. + * @return array + */ +function wc_admin_add_settings( $settings ) { + $settings[] = array( + 'id' => 'woocommerce_excluded_report_order_statuses', + 'option_key' => 'woocommerce_excluded_report_order_statuses', + 'label' => __( 'Excluded report order statuses', 'wc-admin' ), + 'description' => __( 'Statuses that should not be included when calculating report totals.', 'wc-admin' ), + 'default' => '', + 'type' => 'multiselect', + 'options' => format_order_statuses( wc_get_order_statuses() ), + ); + return $settings; +}; +add_filter( 'woocommerce_settings-wc_admin', 'wc_admin_add_settings' ); diff --git a/plugins/woocommerce-admin/lib/client-assets.php b/plugins/woocommerce-admin/lib/client-assets.php index 5725689e037..8de11211e06 100644 --- a/plugins/woocommerce-admin/lib/client-assets.php +++ b/plugins/woocommerce-admin/lib/client-assets.php @@ -206,10 +206,13 @@ function wc_admin_print_script_settings() { ), 'currentUserData' => $current_user_data, ); + $settings = wc_admin_add_custom_settings( $settings ); foreach ( $preload_data_endpoints as $key => $endpoint ) { $settings['dataEndpoints'][ $key ] = $preload_data[ $endpoint ]['body']; } + + $settings = apply_filters( 'wc_admin_wc_settings', $settings ); ?>