Merge pull request woocommerce/woocommerce-admin#420 from woocommerce/fix/402_403

Added reporting REST API extensions to wc-admin.
This commit is contained in:
Claudio Sanches 2018-09-18 16:42:26 -03:00 committed by GitHub
commit da4a2813b8
32 changed files with 6352 additions and 0 deletions

View File

@ -0,0 +1,280 @@
<?php
/**
* REST API Reports categories controller
*
* Handles requests to the /reports/categories endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports categories controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Categories_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/categories';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['categories'] = (array) $request['categories'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$categories_query = new WC_Admin_Reports_Categories_Query( $query_args );
$report_data = $categories_query->get_data();
if ( is_wp_error( $report_data ) ) {
return $report_data;
}
if ( ! isset( $report_data->data ) || ! isset( $report_data->page_no ) || ! isset( $report_data->pages ) ) {
return new WP_Error( 'woocommerce_rest_reports_categories_invalid_response', __( 'Invalid response from data store.', 'woocommerce' ), array( 'status' => 500 ) );
}
$out_data = array();
foreach ( $report_data->data as $datum ) {
$item = $this->prepare_item_for_response( $datum, $request );
$out_data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_categories', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Admin_Reports_Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'category' => array(
'href' => rest_url( sprintf( '/%s/products/categories/%d', $this->namespace, $object['category_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_categories',
'type' => 'object',
'properties' => array(
'category_id' => array(
'description' => __( 'Category ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'items_sold' => array(
'description' => __( 'Amount of items sold.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'gross_revenue' => array(
'description' => __( 'Gross revenue.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'products_count' => array(
'description' => __( 'Amount of products.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'items_sold',
'gross_revenue',
'orders_count',
'products_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['categories'] = array(
'description' => __( 'Limit result set to all items that have the specified term assigned in the categories taxonomy.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
}

View File

@ -0,0 +1,217 @@
<?php
/**
* REST API Reports controller extended by WC Admin plugin.
*
* Handles requests to the reports endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports controller class.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports';
/**
* Register the routes for reports.
*/
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Check whether a given request has permission to read reports.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$data = array();
$reports = array(
array(
'slug' => 'revenue/stats',
'description' => __( 'Stats about revenue.', 'woocommerce' ),
),
array(
'slug' => 'orders/stats',
'description' => __( 'Stats about orders.', 'woocommerce' ),
),
array(
'slug' => 'products',
'description' => __( 'Products detailed reports.', 'woocommerce' ),
),
array(
'slug' => 'products/stats',
'description' => __( 'Stats about products.', 'woocommerce' ),
),
array(
'slug' => 'categories',
'description' => __( 'Product categories detailed reports.', 'woocommerce' ),
),
array(
'slug' => 'categories/stats',
'description' => __( 'Stats about product categories.', 'woocommerce' ),
),
array(
'slug' => 'coupons',
'description' => __( 'Coupons detailed reports.', 'woocommerce' ),
),
array(
'slug' => 'coupons/stats',
'description' => __( 'Stats about coupons.', 'woocommerce' ),
),
array(
'slug' => 'taxes',
'description' => __( 'Taxes detailed reports.', 'woocommerce' ),
),
array(
'slug' => 'taxes/stats',
'description' => __( 'Stats about taxes.', 'woocommerce' ),
),
array(
'slug' => 'downloads',
'description' => __( 'Product downloads detailed reports.', 'woocommerce' ),
),
array(
'slug' => 'downloads/files',
'description' => __( 'Product download files detailed reports.', 'woocommerce' ),
),
array(
'slug' => 'downloads/stats',
'description' => __( 'Stats about product downloads.', 'woocommerce' ),
),
array(
'slug' => 'customers',
'description' => __( 'Customers detailed reports.', 'woocommerce' ),
),
);
foreach ( $reports as $report ) {
$item = $this->prepare_item_for_response( (object) $report, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
return rest_ensure_response( $data );
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = array(
'slug' => $report->slug,
'description' => $report->description,
);
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( array(
'self' => array(
'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ),
),
'collection' => array(
'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
),
) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report',
'type' => 'object',
'properties' => array(
'slug' => array(
'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'A human-readable description of the resource.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
return array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
}
}

View File

@ -0,0 +1,263 @@
<?php
/**
* REST API Reports coupons controller
*
* Handles requests to the /reports/coupons endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports coupons controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Coupons_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/coupons';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['code'] = (array) $request['code'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$coupons_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class.
$report_data = $coupons_query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( (object) $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_coupons', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Reports_Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'coupon' => array(
'href' => rest_url( sprintf( '/%s/coupons/%d', $this->namespace, $object->coupon_id ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_coupons',
'type' => 'object',
'properties' => array(
'coupon_id' => array(
'description' => __( 'Coupon ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'gross_discount' => array(
'description' => __( 'Gross discount.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'items_sold',
'gross_revenue',
'orders_count',
'products_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['coupons'] = array(
'description' => __( 'Limit result set to items assigned one or more code.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
return $params;
}
}

View File

@ -0,0 +1,306 @@
<?php
/**
* REST API Reports coupons stats controller
*
* Handles requests to the /reports/coupons/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports coupons stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Coupons_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/coupons/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['code'] = (array) $request['code'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$coupons_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class.
$report_data = $coupons_query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( (object) $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_coupons_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'gross_discount' => array(
'description' => __( 'Gross discount.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'coupons_count' => array(
'description' => __( 'Amount of coupons.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_coupons_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'items_sold',
'gross_revenue',
'orders_count',
'products_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['coupons'] = array(
'description' => __( 'Limit result set to items assigned one or more code.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
return $params;
}
}

View File

@ -0,0 +1,304 @@
<?php
/**
* REST API Reports customers controller
*
* Handles requests to the /reports/customers endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports customers controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/customers';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @return array
*/
protected function prepare_reports_query( $request ) {
$args = array();
$args['before'] = $request['before'];
$args['after'] = $request['after'];
$args['page'] = $request['page'];
$args['per_page'] = $request['per_page'];
$args['name'] = $request['name'];
$args['username'] = $request['username'];
$args['email'] = $request['email'];
$args['country'] = $request['country'];
$args['last_active_before'] = $request['last_active_before'];
$args['last_active_after'] = $request['last_active_after'];
$args['order_count_min'] = $request['order_count_min'];
$args['order_count_max'] = $request['order_count_max'];
$args['total_spend_min'] = $request['total_spend_min'];
$args['total_spend_max'] = $request['total_spend_max'];
$args['avg_order_value_min'] = $request['avg_order_value_min'];
$args['avg_order_value_max'] = $request['avg_order_value_max'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$customers_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class.
$report_data = $customers_query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'customers' => array(),
);
foreach ( $report_data->customers as $customer_data ) {
$item_data = $this->prepare_item_for_response( (object) $customer_data, $request );
$out_data['customers'][] = $item_data;
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_customers', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Reports_Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'customer' => array(
'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->customer_id ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_customers',
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'customer_id' => array(
'description' => __( 'Customer ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_last_active' => array(
'description' => __( 'Date last active.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_last_active_gmt' => array(
'description' => __( 'Date last active GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Order count.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_spend' => array(
'description' => __( 'Total spend.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_order_value' => array(
'description' => __( 'Avg order value.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['name'] = array(
'description' => __( 'Limit response to objects with a specfic customer name.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['username'] = array(
'description' => __( 'Limit response to objects with a specfic username.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['email'] = array(
'description' => __( 'Limit response to objects equal to an email.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['country'] = array(
'description' => __( 'Limit response to objects with a specfic country.', 'woocommerce' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_before'] = array(
'description' => __( 'Limit response to objects last active before (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['last_active_after'] = array(
'description' => __( 'Limit response to objects last active after (or at) a given ISO8601 compliant datetime.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order_count_min'] = array(
'description' => __( 'Limit response to objects with an order count greater than or equal to given integer.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order_count_max'] = array(
'description' => __( 'Limit response to objects with an order count less than or equal to given integer.', 'woocommerce' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_min'] = array(
'description' => __( 'Limit response to objects with a total order spend greater than or equal to given number.', 'woocommerce' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['total_spend_max'] = array(
'description' => __( 'Limit response to objects with a total order spend less than or equal to given number.', 'woocommerce' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_min'] = array(
'description' => __( 'Limit response to objects with an average order spend greater than or equal to given number.', 'woocommerce' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
$params['avg_order_value_max'] = array(
'description' => __( 'Limit response to objects with an average order spend less than or equal to given number.', 'woocommerce' ),
'type' => 'number',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* REST API Reports downloads controller
*
* Handles requests to the /reports/downloads endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports downloads controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Downloads_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/downloads';
}

View File

@ -0,0 +1,33 @@
<?php
/**
* REST API Reports downloads files controller
*
* Handles requests to the /reports/downloads/files endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports downloads files controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Downloads_Files_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/downloads/files';
}

View File

@ -0,0 +1,33 @@
<?php
/**
* REST API Reports downloads stats controller
*
* Handles requests to the /reports/downloads/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports downloads stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Downloads_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/downloads/stats';
}

View File

@ -0,0 +1,348 @@
<?php
/**
* REST API Reports orders stats controller
*
* Handles requests to the /reports/orders/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports orders stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/orders/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['categories'] = (array) $request['categories'];
$args['coupons'] = (array) $request['coupons'];
$args['products'] = (array) $request['products'];
$args['order_status'] = (array) $request['order_status'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
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();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_orders_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'net_revenue' => array(
'description' => __( 'Net revenue.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_order_value' => array(
'description' => __( 'Average order value.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'avg_items_per_order' => array(
'description' => __( 'Average items per order', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_orders_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'net_revenue',
'orders_count',
'avg_order_value',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['categories'] = array(
'description' => __( 'Limit result set to all items that have the specified term assigned in the categories taxonomy.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['coupons'] = array(
'description' => __( 'Limit result set to all items that have the specified coupon assigned.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['products'] = array(
'description' => __( 'Limit result set to all items that have the specified product assigned.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order_status'] = array(
'default' => array( 'completed', 'processing', 'on-hold' ),
'description' => __( 'Limit result set to orders assigned one or more statuses', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'enum' => $this->get_order_statuses(),
'type' => 'string',
),
);
return $params;
}
/**
* Get order statuses without prefixes.
*
* @return array
*/
protected function get_order_statuses() {
$order_statuses = array();
foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
$order_statuses[] = str_replace( 'wc-', '', $status );
}
return $order_statuses;
}
}

View File

@ -0,0 +1,255 @@
<?php
/**
* REST API Reports products controller
*
* Handles requests to the /reports/products endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports products controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Products_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/products';
/**
* Get items.
*
* @param WP_REST_Request $request Request data.
*
* @return array|WP_Error
*/
public function get_items( $request ) {
$args = array();
$registered = array_keys( $this->get_collection_params() );
foreach ( $registered as $param_name ) {
if ( isset( $request[ $param_name ] ) ) {
$args[ $param_name ] = $request[ $param_name ];
}
}
$reports = new WC_Admin_Reports_Products_Query( $args );
$products_data = $reports->get_data();
$data = array();
foreach ( $products_data->data as $product_data ) {
$item = $this->prepare_item_for_response( $product_data, $request );
$data[] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $data );
$response->header( 'X-WP-Total', (int) $products_data->total );
$response->header( 'X-WP-TotalPages', (int) $products_data->pages );
$page = $products_data->page_no;
$max_pages = $products_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_products', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param Array $object Object data.
* @return array Links for the given post.
*/
protected function prepare_links( $object ) {
$links = array(
'product' => array(
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, 'products', $object['product_id'] ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_products',
'type' => 'object',
'properties' => array(
'product_id' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Product ID.', 'woocommerce' ),
),
'items_sold' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Number of items sold.', 'woocommerce' ),
),
'gross_revenue' => array(
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Total gross revenue of all items sold.', 'woocommerce' ),
),
'orders_count' => array(
'type' => 'integer',
'readonly' => true,
'context' => array( 'view', 'edit' ),
'description' => __( 'Number of orders product appeared in.', 'woocommerce' ),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'gross_revenue',
'orders_count',
'items_sold',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['categories'] = array(
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['products'] = array(
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['extended_product_info'] = array(
'description' => __( 'Add additional piece of info about each product to the report.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@ -0,0 +1,312 @@
<?php
/**
* REST API Reports products stats controller
*
* Handles requests to the /reports/products/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports products stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Products_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/products/stats';
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = array(
'fields' => array(
'items_sold',
'gross_revenue',
'orders_count',
'products_count',
),
);
foreach ( array_keys( $this->get_collection_params() ) as $arg ) {
if ( isset( $request[ $arg ] ) ) {
$query_args[ $arg ] = $request[ $arg ];
}
}
$query = new WC_Admin_Reports_Products_Stats_Query( $query_args );
$report_data = $query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_products_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'items_sold' => array(
'description' => __( 'Number of items sold.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'gross_revenue' => array(
'description' => __( 'Gross revenue.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Number of orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_products_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'gross_revenue',
'coupons',
'refunds',
'shipping',
'taxes',
'net_revenue',
'orders_count',
'items_sold',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['categories'] = array(
'description' => __( 'Limit result to items from the specified categories.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
$params['products'] = array(
'description' => __( 'Limit result to items with specified product ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
}

View File

@ -0,0 +1,323 @@
<?php
/**
* REST API Reports revenue stats controller
*
* Handles requests to the /reports/revenue/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports revenue stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Revenue_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/revenue/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
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();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param Array $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = $report;
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_revenue_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'gross_revenue' => array(
'description' => __( 'Gross revenue.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'net_revenue' => array(
'description' => __( 'Net revenue.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'coupons' => array(
'description' => __( 'Total of coupons.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping' => array(
'description' => __( 'Total of shipping.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'taxes' => array(
'description' => __( 'Total of taxes.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'refunds' => array(
'description' => __( 'Total of refunds.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_revenue_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'gross_revenue',
'coupons',
'refunds',
'shipping',
'taxes',
'net_revenue',
'orders_count',
'items_sold',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
return $params;
}
}

View File

@ -0,0 +1,254 @@
<?php
/**
* REST API Reports taxes controller
*
* Handles requests to the /reports/taxes endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports taxes controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Taxes_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/taxes';
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$taxes_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class.
$report_data = $taxes_query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( (object) $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $report ) );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_taxes', $response, $report, $request );
}
/**
* Prepare links for the request.
*
* @param WC_Reports_Query $object Object data.
* @return array
*/
protected function prepare_links( $object ) {
$links = array(
'tax' => array(
'href' => rest_url( sprintf( '/%s/taxes/%d', $this->namespace, $object->category_id ) ),
),
);
return $links;
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_taxes',
'type' => 'object',
'properties' => array(
'tax_rate_id' => array(
'description' => __( 'Tax rate ID.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'total_tax' => array(
'description' => __( 'Total tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order_tax' => array(
'description' => __( 'Order tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_tax' => array(
'description' => __( 'Shipping tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders.', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'name',
'tax_rate_id',
'orders_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['code'] = array(
'description' => __( 'Limit result set to items assigned one or more code.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'string',
),
);
return $params;
}
}

View File

@ -0,0 +1,311 @@
<?php
/**
* REST API Reports taxes stats controller
*
* Handles requests to the /reports/taxes/stats endpoint.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* REST API Reports taxes stats controller class.
*
* @package WooCommerce/API
* @extends WC_REST_Reports_Controller
*/
class WC_Admin_REST_Reports_Taxes_Stats_Controller extends WC_REST_Reports_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'reports/taxes/stats';
/**
* Maps query arguments from the REST request.
*
* @param array $request Request array.
* @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['code'] = (array) $request['code'];
return $args;
}
/**
* Get all reports.
*
* @param WP_REST_Request $request Request data.
* @return array|WP_Error
*/
public function get_items( $request ) {
$query_args = $this->prepare_reports_query( $request );
$taxes_query = new WC_Reports_Orders_Stats_Query( $query_args ); // @todo change to correct class.
$report_data = $taxes_query->get_data();
$out_data = array(
'totals' => get_object_vars( $report_data->totals ),
'intervals' => array(),
);
foreach ( $report_data->intervals as $interval_data ) {
$item = $this->prepare_item_for_response( (object) $interval_data, $request );
$out_data['intervals'][] = $this->prepare_response_for_collection( $item );
}
$response = rest_ensure_response( $out_data );
$response->header( 'X-WP-Total', (int) $report_data->total );
$response->header( 'X-WP-TotalPages', (int) $report_data->pages );
$page = $report_data->page_no;
$max_pages = $report_data->pages;
$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) );
if ( $page > 1 ) {
$prev_page = $page - 1;
if ( $prev_page > $max_pages ) {
$prev_page = $max_pages;
}
$prev_link = add_query_arg( 'page', $prev_page, $base );
$response->link_header( 'prev', $prev_link );
}
if ( $max_pages > $page ) {
$next_page = $page + 1;
$next_link = add_query_arg( 'page', $next_page, $base );
$response->link_header( 'next', $next_link );
}
return $response;
}
/**
* Prepare a report object for serialization.
*
* @param stdClass $report Report data.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response
*/
public function prepare_item_for_response( $report, $request ) {
$data = get_object_vars( $report );
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
// Wrap the data in a response object.
$response = rest_ensure_response( $data );
/**
* Filter a report returned from the API.
*
* Allows modification of the report data right before it is returned.
*
* @param WP_REST_Response $response The response object.
* @param object $report The original report object.
* @param WP_REST_Request $request Request used to generate the response.
*/
return apply_filters( 'woocommerce_rest_prepare_report_taxes_stats', $response, $report, $request );
}
/**
* Get the Report's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$totals = array(
'total_tax' => array(
'description' => __( 'Total tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'order_tax' => array(
'description' => __( 'Order tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'shipping_tax' => array(
'description' => __( 'Shipping tax.', 'woocommerce' ),
'type' => 'number',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'orders_count' => array(
'description' => __( 'Amount of orders', 'woocommerce' ),
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
);
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'report_taxes_stats',
'type' => 'object',
'properties' => array(
'totals' => array(
'description' => __( 'Totals data.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
'intervals' => array(
'description' => __( 'Reports data grouped by intervals.', 'woocommerce' ),
'type' => 'array',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'items' => array(
'type' => 'object',
'properties' => array(
'interval' => array(
'description' => __( 'Type of interval.', 'woocommerce' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'enum' => array( 'day', 'week', 'month', 'year' ),
),
'date_start' => array(
'description' => __( "The date the report start, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_start_gmt' => array(
'description' => __( 'The date the report start, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end' => array(
'description' => __( "The date the report end, in the site's timezone.", 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'date_end_gmt' => array(
'description' => __( 'The date the report end, as GMT.', 'woocommerce' ),
'type' => 'date-time',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'subtotals' => array(
'description' => __( 'Interval subtotals.', 'woocommerce' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
'properties' => $totals,
),
),
),
),
),
);
return $this->add_additional_fields_schema( $schema );
}
/**
* Get the query params for collections.
*
* @return array
*/
public function get_collection_params() {
$params = array();
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
$params['page'] = array(
'description' => __( 'Current page of the collection.', 'woocommerce' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
);
$params['per_page'] = array(
'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['before'] = array(
'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',
'format' => 'date-time',
'validate_callback' => 'rest_validate_request_arg',
);
$params['order'] = array(
'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ),
'type' => 'string',
'default' => 'desc',
'enum' => array( 'asc', 'desc' ),
'validate_callback' => 'rest_validate_request_arg',
);
$params['orderby'] = array(
'description' => __( 'Sort collection by object attribute.', 'woocommerce' ),
'type' => 'string',
'default' => 'date',
'enum' => array(
'date',
'items_sold',
'gross_revenue',
'orders_count',
'products_count',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['interval'] = array(
'description' => __( 'Time interval to use for buckets in the returned data.', 'woocommerce' ),
'type' => 'string',
'default' => 'week',
'enum' => array(
'hour',
'day',
'week',
'month',
'quarter',
'year',
),
'validate_callback' => 'rest_validate_request_arg',
);
$params['taxes'] = array(
'description' => __( 'Limit result set to all items that have the specified term assigned in the taxes taxonomy.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
'items' => array(
'type' => 'integer',
),
);
return $params;
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* REST API WC System Status Tools Controller
*
* Handles requests to the /system_status/tools/* endpoints.
*
* @package WooCommerce Admin/API
*/
defined( 'ABSPATH' ) || exit;
/**
* System status tools controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_System_Status_Tools_Controller
*/
class WC_Admin_REST_System_Status_Tools_Controller extends WC_REST_System_Status_Tools_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc/v3';
/**
* A list of available tools for use in the system status section.
* 'button' becomes 'action' in the API.
*
* @return array
*/
public function get_tools() {
return array_merge(
parent::get_tools(),
array(
'rebuild_stats' => array(
'name' => __( 'Rebuild reports data', 'woocommerce' ),
'button' => __( 'Rebuild reports', 'woocommerce' ),
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'woocommerce' ),
),
)
);
}
/**
* Actually executes a tool.
*
* @param string $tool Tool.
* @return array
*/
public function execute_tool( $tool ) {
$ran = true;
$message = '';
switch ( $tool ) {
case 'rebuild_stats':
WC_Admin_Reports_Orders_Data_Store::queue_order_stats_repopulate_database();
$message = __( 'Rebuilding reports data in the background . . .', 'woocommerce' );
break;
default:
return parent::execute_tool( $tool );
}
return array(
'success' => $ran,
'message' => $message,
);
}
}

View File

@ -0,0 +1,342 @@
<?php
/**
* REST API bootstrap.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Api_Init class.
*/
class WC_Admin_Api_Init {
/**
* Boostrap REST API.
*/
public function __construct() {
// Initialize classes.
add_action( 'plugins_loaded', array( $this, 'init_classes' ), 19 );
// Hook in data stores.
add_filter( 'woocommerce_data_stores', array( 'WC_Admin_Api_Init', 'add_data_stores' ) );
// Add wc-admin report tables to list of WooCommerce tables.
add_filter( 'woocommerce_install_get_tables', array( 'WC_Admin_Api_Init', 'add_report_tables' ) );
// REST API extensions init.
add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
add_filter( 'rest_endpoints', array( 'WC_Admin_Api_Init', 'filter_rest_endpoints' ), 10, 1 );
add_filter( 'woocommerce_debug_tools', array( 'WC_Admin_Api_Init', 'add_regenerate_tool' ) );
// Initialize report classes.
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'orders_data_store_init' ), 20 );
add_action( 'woocommerce_after_register_post_type', array( 'WC_Admin_Api_Init', 'order_product_lookup_store_init' ), 20 );
// Create tables.
$this->create_db_tables();
}
/**
* Init classes.
*/
public function init_classes() {
// Interfaces.
require_once dirname( __FILE__ ) . '/interfaces/class-wc-admin-reports-data-store-interface.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-query.php';
// Common date time code.
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-interval.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-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-products-stats-query.php';
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-categories-query.php';
// Reports data stores.
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-orders-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-products-stats-data-store.php';
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-categories-data-store.php';
}
/**
* Init REST API.
*/
public function rest_api_init() {
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-system-status-tools-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-categories-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-coupons-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-coupons-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-customers-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-files-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-downloads-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-orders-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-products-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-products-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-revenue-stats-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
$controllers = array(
'WC_Admin_REST_Reports_Controller',
'WC_Admin_REST_System_Status_Tools_Controller',
'WC_Admin_REST_Reports_Products_Controller',
'WC_Admin_REST_Reports_Products_Stats_Controller',
'WC_Admin_REST_Reports_Revenue_Stats_Controller',
'WC_Admin_REST_Reports_Orders_Stats_Controller',
'WC_Admin_REST_Reports_Categories_Controller',
'WC_Admin_REST_Reports_Taxes_Controller',
'WC_Admin_REST_Reports_Taxes_Stats_Controller',
'WC_Admin_REST_Reports_Coupons_Controller',
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
);
foreach ( $controllers as $controller ) {
$this->$controller = new $controller();
$this->$controller->register_routes();
}
}
/**
* Filter REST API endpoints.
*
* @param array $endpoints List of endpoints.
* @return array
*/
public static function filter_rest_endpoints( $endpoints ) {
// Override GET /wc/v3/system_status/tools.
if ( isset( $endpoints['/wc/v3/system_status/tools'] )
&& isset( $endpoints['/wc/v3/system_status/tools'][1] )
&& isset( $endpoints['/wc/v3/system_status/tools'][0] )
&& $endpoints['/wc/v3/system_status/tools'][1]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
) {
$endpoints['/wc/v3/system_status/tools'][0] = $endpoints['/wc/v3/system_status/tools'][1];
}
// // Override GET & PUT for /wc/v3/system_status/tools.
if ( isset( $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'] )
&& isset( $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][3] )
&& isset( $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][2] )
&& $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][2]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
&& $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][3]['callback'][0] instanceof WC_Admin_REST_System_Status_Tools_Controller
) {
$endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][0] = $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][2];
$endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][1] = $endpoints['/wc/v3/system_status/tools/(?P<id>[\w-]+)'][3];
}
// Override GET /wc/v3/reports.
if ( isset( $endpoints['/wc/v3/reports'] )
&& isset( $endpoints['/wc/v3/reports'][1] )
&& isset( $endpoints['/wc/v3/reports'][0] )
&& $endpoints['/wc/v3/reports'][1]['callback'][0] instanceof WC_Admin_REST_Reports_Controller
) {
$endpoints['/wc/v3/reports'][0] = $endpoints['/wc/v3/reports'][1];
}
return $endpoints;
}
/**
* Adds regenerate tool.
*
* @param array $tools List of tools.
* @return array
*/
public static function add_regenerate_tool( $tools ) {
return array_merge(
$tools,
array(
'rebuild_stats' => array(
'name' => __( 'Rebuild reports data', 'woocommerce' ),
'button' => __( 'Rebuild reports', 'woocommerce' ),
'desc' => __( 'This tool will rebuild all of the information used by the reports.', 'woocommerce' ),
'callback' => array( 'WC_Admin_Reports_Orders_Data_Store', 'queue_order_stats_repopulate_database' ),
),
)
);
}
/**
* Init orders data store.
*/
public static function orders_data_store_init() {
WC_Admin_Reports_Orders_Data_Store::init();
}
/**
* Init orders product looup store.
*
* @param WC_Background_Updater|null $updater Updater instance.
* @return bool
*/
public static function order_product_lookup_store_init( $updater = null ) {
global $wpdb;
$orders = get_transient( 'wc_update_350_all_orders' );
if ( false === $orders ) {
$orders = wc_get_orders( array(
'limit' => -1,
'return' => 'ids',
) );
set_transient( 'wc_update_350_all_orders', $orders, DAY_IN_SECONDS );
}
// Process orders until close to running out of memory timeouts on large sites then requeue.
foreach ( $orders as $order_id ) {
$order = wc_get_order( $order_id );
if ( ! $order ) {
continue;
}
foreach ( $order->get_items() as $order_item ) {
$wpdb->replace(
$wpdb->prefix . 'wc_admin_order_product_lookup',
array(
'order_item_id' => $order_item->get_id(),
'order_id' => $order->get_id(),
'product_id' => $order_item->get_product_id( 'edit' ),
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
'product_qty' => $order_item->get_quantity( 'edit' ),
'product_gross_revenue' => $order_item->get_subtotal( 'edit' ),
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
),
array(
'%d',
'%d',
'%d',
'%d',
'%d',
'%f',
'%s',
)
);
}
// Pop the order ID from the array for updating the transient later should we near memory exhaustion.
unset( $orders[ $order_id ] );
if ( $updater instanceof WC_Background_Updater && $updater->is_memory_exceeded() ) {
// Update the transient for the next run to avoid processing the same orders again.
set_transient( 'wc_update_350_all_orders', $orders, DAY_IN_SECONDS );
return true;
}
}
return true;
}
/**
* Adds data stores.
*
* @param array $data_stores List of data stores.
* @return array
*/
public static function add_data_stores( $data_stores ) {
return array_merge(
$data_stores,
array(
'report-revenue-stats' => 'WC_Admin_Reports_Orders_Data_Store',
'report-orders-stats' => 'WC_Admin_Reports_Orders_Data_Store',
'report-products' => 'WC_Admin_Reports_Products_Data_Store',
'report-products-stats' => 'WC_Admin_Reports_Products_Stats_Data_Store',
'report-categories' => 'WC_Admin_Reports_Categories_Data_Store',
)
);
}
/**
* Adds report tables.
*
* @param array $wc_tables List of WooCommerce tables.
* @return array
*/
public static function add_report_tables( $wc_tables ) {
global $wpdb;
return array_merge(
$wc_tables,
array(
// TODO: will this work on multisite?
"{$wpdb->prefix}wc_admin_order_stats",
"{$wpdb->prefix}wc_admin_order_product_lookup",
"{$wpdb->prefix}wc_order_tax_lookup",
"{$wpdb->prefix}wc_order_coupon_lookup",
)
);
}
/**
* Get database schema.
*
* @return string
*/
private static function get_schema() {
global $wpdb;
if ( $wpdb->has_cap( 'collation' ) ) {
$collate = $wpdb->get_charset_collate();
}
$tables = "
CREATE TABLE {$wpdb->prefix}wc_admin_order_stats (
order_id bigint(20) unsigned NOT NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
num_items_sold int(11) UNSIGNED DEFAULT 0 NOT NULL,
gross_total double DEFAULT 0 NOT NULL,
coupon_total double DEFAULT 0 NOT NULL,
refund_total double DEFAULT 0 NOT NULL,
tax_total double DEFAULT 0 NOT NULL,
shipping_total double DEFAULT 0 NOT NULL,
net_total double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_admin_order_product_lookup (
order_item_id BIGINT UNSIGNED NOT NULL,
order_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
customer_id BIGINT UNSIGNED NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
product_qty INT UNSIGNED NOT NULL,
product_gross_revenue double DEFAULT 0 NOT NULL,
PRIMARY KEY (order_item_id),
KEY order_id (order_id),
KEY product_id (product_id),
KEY customer_id (customer_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_tax_lookup (
order_id BIGINT UNSIGNED NOT NULL,
tax_rate_id BIGINT UNSIGNED NOT NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
shipping_tax double DEFAULT 0 NOT NULL,
order_tax double DEFAULT 0 NOT NULL,
total_tax double DEFAULT 0 NOT NULL,
KEY order_id (order_id),
KEY tax_rate_id (tax_rate_id),
KEY date_created (date_created)
) $collate;
CREATE TABLE {$wpdb->prefix}wc_order_coupon_lookup (
order_id BIGINT UNSIGNED NOT NULL,
coupon_id BIGINT UNSIGNED NOT NULL,
date_created timestamp DEFAULT '0000-00-00 00:00:00' NOT NULL,
coupon_gross_discount double DEFAULT 0 NOT NULL,
KEY order_id (order_id),
KEY coupon_id (coupon_id),
KEY date_created (date_created)
) $collate;";
return $tables;
}
/**
* Create database tables.
*/
protected function create_db_tables() {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( self::get_schema() );
}
}
new WC_Admin_Api_Init();

View File

@ -0,0 +1,77 @@
<?php
/**
* Order stats background process.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_Background_Process', false ) ) {
include_once WC_ABSPATH . '/includes/abstracts/class-wc-background-process.php';
}
/**
* WC_Admin_Order_Stats_Background_Process class.
*
* @todo use Action Scheduler instead of this.
*/
class WC_Admin_Order_Stats_Background_Process extends WC_Background_Process {
/**
* Initiate new background process.
*/
public function __construct() {
// Uses unique prefix per blog so each blog has separate queue.
$this->prefix = 'wp_' . get_current_blog_id();
$this->action = 'wc_order_stats';
parent::__construct();
}
/**
* Push to queue without scheduling duplicate recalculation events.
* Overrides WC_Background_Process::push_to_queue.
*
* @param integer $data Timestamp of hour to generate stats.
*/
public function push_to_queue( $data ) {
$data = absint( $data );
if ( ! in_array( $data, $this->data, true ) ) {
$this->data[] = $data;
}
return $this;
}
/**
* Dispatch but only if there is data to update.
* Overrides WC_Background_Process::dispatch.
*/
public function dispatch() {
if ( ! $this->data ) {
return false;
}
return parent::dispatch();
}
/**
* Code to execute for each item in the queue
*
* @param string $item Queue item to iterate over.
* @return bool
*/
protected function task( $item ) {
if ( ! $item ) {
return false;
}
$order = wc_get_order( $item );
if ( ! $order ) {
return false;
}
WC_Admin_Reports_Orders_Data_Store::update( $order );
return false;
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Class for parameter-based Categories Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'order' => 'desc',
* 'orderby' => 'items_sold',
* );
* $report = new WC_Admin_Reports_Categories_Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Categories_Query
*
*/
class WC_Admin_Reports_Categories_Query extends WC_Admin_Reports_Query {
const REPORT_NAME = 'report-categories';
/**
* Valid fields for Categories report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get categories data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_reports_categories_query_args', $this->get_query_vars() );
$results = WC_Data_Store::load( self::REPORT_NAME )->get_data( $args );
return apply_filters( 'woocommerce_reports_categories_select_query', $results, $args );
}
}

View File

@ -0,0 +1,410 @@
<?php
/**
* Class for time interval handling for reports
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* Date & time interval handling class for Reporting API.
*/
class WC_Admin_Reports_Interval {
/**
* Format string for ISO DateTime formatter.
*
* @var string
*/
public static $iso_datetime_format = 'Y-m-d\TH:i:s\Z';
/**
* Format string for use in SQL queries.
*
* @var string
*/
public static $sql_datetime_format = 'Y-m-d H:i:s';
/**
* Returns date format to be used as grouping clause in SQL.
*
* @param string $time_interval Time interval.
* @return mixed
*/
public static function mysql_datetime_format( $time_interval ) {
$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')";
} 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'))";
}
$mysql_date_format_mapping = array(
'hour' => "DATE_FORMAT(date_created, '%Y-%m-%d %k')",
'day' => "DATE_FORMAT(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)',
);
return $mysql_date_format_mapping[ $time_interval ];
}
/**
* Returns quarter for the DateTime.
*
* @param DateTime $datetime Date & time.
* @return int|null
*/
public static function quarter( $datetime ) {
switch ( (int) $datetime->format( 'm' ) ) {
case 1:
case 2:
case 3:
return 1;
case 4:
case 5:
case 6:
return 2;
case 7:
case 8:
case 9:
return 3;
case 10:
case 11:
case 12:
return 4;
}
return null;
}
/**
* Returns simple week number for the DateTime, for week starting on $first_day_of_week.
*
* The first week of the year is considered to be the week containing January 1.
* The second week starts on the next $first_day_of_week.
*
* @param DateTime $datetime Date for which the week number is to be calculated.
* @param int $first_day_of_week 0 for Sunday to 6 for Saturday.
* @return int
*/
public static function simple_week_number( $datetime, $first_day_of_week ) {
$beg_of_year_day = new DateTime( "{$datetime->format('Y')}-01-01" );
$adj_day_beg_of_year = ( (int) $beg_of_year_day->format( 'w' ) - $first_day_of_week + 7 ) % 7;
$days_since_start_of_year = (int) $datetime->format( 'z' ) + 1;
return (int) floor( ( ( $days_since_start_of_year + $adj_day_beg_of_year - 1 ) / 7 ) ) + 1;
}
/**
* Returns ISO 8601 week number for the DateTime, if week starts on Monday,
* otherwise returns simple week number.
*
* @see WC_Admin_Reports_Interval::simple_week_number()
*
* @param DateTime $datetime Date for which the week number is to be calculated.
* @param int $first_day_of_week 0 for Sunday to 6 for Saturday.
* @return int
*/
public static function week_number( $datetime, $first_day_of_week ) {
if ( 1 === $first_day_of_week ) {
$week_number = (int) $datetime->format( 'W' );
} else {
$week_number = WC_Admin_Reports_Interval::simple_week_number( $datetime, $first_day_of_week );
}
return $week_number;
}
/**
* Returns time interval id for the DateTime.
*
* @param string $time_interval Time interval type (week, day, etc).
* @param DateTime $datetime Date & time.
* @return string
*/
public static function time_interval_id( $time_interval, $datetime ) {
$php_time_format_for = array(
'hour' => 'Y-m-d H',
'day' => 'Y-m-d',
'week' => 'o-W',
'month' => 'Y-m',
'quarter' => 'Y-' . WC_Admin_Reports_Interval::quarter( $datetime ),
'year' => 'Y',
);
// If the week does not begin on Monday.
$first_day_of_week = absint( get_option( 'start_of_week' ) );
if ( 'week' === $time_interval && 1 !== $first_day_of_week ) {
$week_no = WC_Admin_Reports_Interval::simple_week_number( $datetime, $first_day_of_week );
$week_no = str_pad( $week_no, 2, '0', STR_PAD_LEFT );
$year_no = $datetime->format( 'Y' );
return "$year_no-$week_no";
}
return $datetime->format( $php_time_format_for[ $time_interval ] );
}
/**
* Calculates number of time intervals between two dates, closed interval on both sides.
*
* @param string $start Start date & time.
* @param string $end End date & time.
* @param string $interval Time interval increment, e.g. hour, day, week.
* @return int
*/
public static function intervals_between( $start, $end, $interval ) {
$start_datetime = new DateTime( $start );
$end_datetime = new DateTime( $end );
switch ( $interval ) {
case 'hour':
$diff_timestamp = (int) $end_datetime->format( 'U' ) - (int) $start_datetime->format( 'U' );
return (int) floor( ( (int) $diff_timestamp ) / HOUR_IN_SECONDS ) + 1;
case 'day':
$diff_timestamp = (int) $end_datetime->format( 'U' ) - (int) $start_datetime->format( 'U' );
return (int) floor( ( (int) $diff_timestamp ) / DAY_IN_SECONDS ) + 1;
case 'week':
// TODO: optimize? approximately day count / 7, but year end is tricky, a week can have fewer days.
$week_count = 0;
do {
$start_datetime = WC_Admin_Reports_Interval::next_week_start( $start_datetime );
$week_count++;
} while ( $start_datetime <= $end_datetime );
return $week_count;
case 'month':
// Year diff in months: (end_year - start_year - 1) * 12.
$year_diff_in_months = ( (int) $end_datetime->format( 'Y' ) - (int) $start_datetime->format( 'Y' ) - 1 ) * 12;
// All the months in end_date year plus months from X to 12 in the start_date year.
$month_diff = (int) $end_datetime->format( 'n' ) + ( 12 - (int) $start_datetime->format( 'n' ) );
// Add months for number of years between end_date and start_date.
$month_diff += $year_diff_in_months + 1;
return $month_diff;
case 'quarter':
// Year diff in quarters: (end_year - start_year - 1) * 4.
$year_diff_in_quarters = ( (int) $end_datetime->format( 'Y' ) - (int) $start_datetime->format( 'Y' ) - 1 ) * 4;
// All the quarters in end_date year plus quarters from X to 4 in the start_date year.
$quarter_diff = WC_Admin_Reports_Interval::quarter( $end_datetime ) + ( 4 - WC_Admin_Reports_Interval::quarter( $start_datetime ) );
// Add quarters for number of years between end_date and start_date.
$quarter_diff += $year_diff_in_quarters + 1;
return $quarter_diff;
case 'year':
$year_diff = (int) $end_datetime->format( 'Y' ) - (int) $start_datetime->format( 'Y' );
return $year_diff + 1;
}
return 0;
}
/**
* Returns a new DateTime object representing the next hour start/previous hour end if reversed.
*
* @param DateTime $datetime Date and time.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function next_hour_start( $datetime, $reversed = false ) {
$hour_increment = $reversed ? 0 : 1;
$timestamp = (int) $datetime->format( 'U' );
$hours_offset_timestamp = ( floor( $timestamp / HOUR_IN_SECONDS ) + $hour_increment ) * HOUR_IN_SECONDS;
if ( $reversed ) {
$hours_offset_timestamp --;
}
$hours_offset_time = new DateTime();
$hours_offset_time->setTimestamp( $hours_offset_timestamp );
return $hours_offset_time;
}
/**
* Returns a new DateTime object representing the next day start, or previous day end if reversed.
*
* @param DateTime $datetime Date and time.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function next_day_start( $datetime, $reversed = false ) {
$day_increment = $reversed ? -1 : 1;
$timestamp = (int) $datetime->format( 'U' );
$next_day_timestamp = ( floor( $timestamp / DAY_IN_SECONDS ) + $day_increment ) * DAY_IN_SECONDS;
$next_day = new DateTime();
$next_day->setTimestamp( $next_day_timestamp );
// The day boundary is actually next midnight when going in reverse, so set it to day -1 at 23:59:59.
if ( $reversed ) {
$timestamp = (int) $next_day->format( 'U' );
$end_of_day_timestamp = floor( $timestamp / DAY_IN_SECONDS ) * DAY_IN_SECONDS + DAY_IN_SECONDS - 1;
$next_day->setTimestamp( $end_of_day_timestamp );
}
return $next_day;
}
/**
* Returns DateTime object representing the next week start, or previous week end if reversed.
*
* @param DateTime $datetime Date and time.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function next_week_start( $datetime, $reversed = false ) {
$first_day_of_week = absint( get_option( 'start_of_week' ) );
$initial_week_no = WC_Admin_Reports_Interval::week_number( $datetime, $first_day_of_week );
do {
$datetime = WC_Admin_Reports_Interval::next_day_start( $datetime, $reversed );
$current_week_no = WC_Admin_Reports_Interval::week_number( $datetime, $first_day_of_week );
} while ( $current_week_no === $initial_week_no );
// The week boundary is actually next midnight when going in reverse, so set it to day -1 at 23:59:59.
if ( $reversed ) {
$timestamp = (int) $datetime->format( 'U' );
$end_of_day_timestamp = floor( $timestamp / DAY_IN_SECONDS ) * DAY_IN_SECONDS + DAY_IN_SECONDS - 1;
$datetime->setTimestamp( $end_of_day_timestamp );
}
return $datetime;
}
/**
* Returns a new DateTime object representing the next month start, or previous month end if reversed.
*
* @param DateTime $datetime Date and time.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function next_month_start( $datetime, $reversed = false ) {
$month_increment = 1;
$year = $datetime->format( 'Y' );
$month = (int) $datetime->format( 'm' );
if ( $reversed ) {
$beg_of_month_datetime = new DateTime( "$year-$month-01 00:00:00" );
$timestamp = (int) $beg_of_month_datetime->format( 'U' );
$end_of_prev_month_timestamp = $timestamp - 1;
$datetime->setTimestamp( $end_of_prev_month_timestamp );
} else {
$month += $month_increment;
if ( $month > 12 ) {
$month = 1;
$year ++;
}
$day = '01';
$datetime = new DateTime( "$year-$month-$day 00:00:00" );
}
return $datetime;
}
/**
* Returns a new DateTime object representing the next quarter start, or previous quarter end if reversed.
*
* @param DateTime $datetime Date and time.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function next_quarter_start( $datetime, $reversed = false ) {
$year = $datetime->format( 'Y' );
$month = (int) $datetime->format( 'n' );
switch ( $month ) {
case 1:
case 2:
case 3:
if ( $reversed ) {
$month = 1;
} else {
$month = 4;
}
break;
case 4:
case 5:
case 6:
if ( $reversed ) {
$month = 4;
} else {
$month = 7;
}
break;
case 7:
case 8:
case 9:
if ( $reversed ) {
$month = 7;
} else {
$month = 10;
}
break;
case 10:
case 11:
case 12:
if ( $reversed ) {
$month = 10;
} else {
$month = 1;
$year ++;
}
break;
}
$datetime = new DateTime( "$year-$month-01 00:00:00" );
if ( $reversed ) {
$timestamp = (int) $datetime->format( 'U' );
$end_of_prev_month_timestamp = $timestamp - 1;
$datetime->setTimestamp( $end_of_prev_month_timestamp );
}
return $datetime;
}
/**
* Return a new DateTime object representing the next year start, or previous year end if reversed.
*
* @param DateTime $datetime Date and time.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function next_year_start( $datetime, $reversed = false ) {
$year_increment = 1;
$year = (int) $datetime->format( 'Y' );
$month = '01';
$day = '01';
if ( $reversed ) {
$datetime = new DateTime( "$year-$month-$day 00:00:00" );
$timestamp = (int) $datetime->format( 'U' );
$end_of_prev_year_timestamp = $timestamp - 1;
$datetime->setTimestamp( $end_of_prev_year_timestamp );
} else {
$year += $year_increment;
$datetime = new DateTime( "$year-$month-$day 00:00:00" );
}
return $datetime;
}
/**
* Returns beginning of next time interval for provided DateTime.
*
* E.g. for current DateTime, beginning of next day, week, quarter, etc.
*
* @param DateTime $datetime Date and time.
* @param string $time_interval Time interval, e.g. week, day, hour.
* @param bool $reversed Going backwards in time instead of forward.
* @return DateTime
*/
public static function iterate( $datetime, $time_interval, $reversed = false ) {
// $result_datetime =
// $result_timestamp_adjusted = $result_datetime->format( 'U' ) - 1;
// $result_datetime->setTimestamp( $result_timestamp_adjusted );
return call_user_func( array( __CLASS__, "next_{$time_interval}_start" ), $datetime, $reversed );
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Class for parameter-based Orders Reports querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'interval' => 'week',
* 'categories' => array(15, 18),
* 'coupons' => array(138),
* 'order_status' => array('completed'),
* );
* $report = new WC_Admin_Reports_Orders_Stats_Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Orders_Stats_Query
*
*/
class WC_Admin_Reports_Orders_Stats_Query extends WC_Admin_Reports_Query {
const REPORT_NAME = 'report-orders-stats';
/**
* Valid fields for Orders report.
*
* @return array
*/
protected function get_default_query_vars() {
return array(
'fields' => array(
'net_revenue',
'avg_order_value',
'orders_count',
'avg_items_per_order',
),
);
}
/**
* Get revenue data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_reports_orders_stats_query_args', $this->get_query_vars() );
$results = WC_Data_Store::load( $this::REPORT_NAME )->get_data( $args );
return apply_filters( 'woocommerce_reports_orders_stats_select_query', $results, $args );
}
}

View File

@ -0,0 +1,50 @@
<?php
/**
* Class for parameter-based Products Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'categories' => array(15, 18),
* 'products' => array(1,2,3)
* );
* $report = new WC_Admin_Reports_Products_Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Products_Query
*
*/
class WC_Admin_Reports_Products_Query extends WC_Admin_Reports_Query {
const REPORT_NAME = 'report-products';
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_reports_products_query_args', $this->get_query_vars() );
$results = WC_Data_Store::load( $this::REPORT_NAME )->get_data( $args );
return apply_filters( 'woocommerce_reports_products_select_query', $results, $args );
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Class for parameter-based Products Stats Report querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'page' => 2,
* 'categories' => array(15, 18),
* 'product_ids' => array(1,2,3)
* );
* $report = new WC_Admin_Reports_Products_Stats_Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Products_Query
*
*/
class WC_Admin_Reports_Products_Stats_Query extends WC_Admin_Reports_Query {
const REPORT_NAME = 'report-products-stats';
/**
* Valid fields for Products report.
*
* @return array
*/
protected function get_default_query_vars() {
return array();
}
/**
* Get product data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_reports_products_stats_query_args', $this->get_query_vars() );
$results = WC_Data_Store::load( $this::REPORT_NAME )->get_data( $args );
return apply_filters( 'woocommerce_reports_products_stats_select_query', $results, $args );
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* Class for parameter-based Reports querying
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Query
*
*/
abstract class WC_Admin_Reports_Query extends WC_Object_Query {
/**
* Get report data matching the current query vars.
*
* @return array|object of WC_Product objects
*/
public function get_data() {
/* translators: %s: Method name */
return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) );
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* Class for parameter-based Revenue Reports querying
*
* Example usage:
* $args = array(
* 'before' => '2018-07-19 00:00:00',
* 'after' => '2018-07-05 00:00:00',
* 'interval' => 'week',
* );
* $report = new WC_Admin_Reports_Revenue_Query( $args );
* $mydata = $report->get_data();
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Revenue_Query
*
*/
class WC_Admin_Reports_Revenue_Query extends WC_Admin_Reports_Query {
const REPORT_NAME = 'report-revenue-stats';
/**
* Valid fields for Revenue report.
*
* @return array
*/
protected function get_default_query_vars() {
return array(
'per_page' => get_option( 'posts_per_page' ), // not sure if this should be the default.
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => '',
'after' => '',
'interval' => 'week',
'fields' => array(
'orders_count',
'num_items_sold',
'gross_revenue',
'coupons',
'refunds',
'taxes',
'shipping',
'net_revenue',
),
);
}
/**
* Get revenue data based on the current query vars.
*
* @return array
*/
public function get_data() {
$args = apply_filters( 'woocommerce_reports_revenue_query_args', $this->get_query_vars() );
$results = WC_Data_Store::load( $this::REPORT_NAME )->get_data( $args );
return apply_filters( 'woocommerce_reports_revenue_select_query', $results, $args );
}
}

View File

@ -0,0 +1,265 @@
<?php
/**
* WC_Admin_Reports_Categories_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Categories_Data_Store.
*
*/
class WC_Admin_Reports_Categories_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Table used to get the data.
*
* @var string
*/
const TABLE_NAME = 'wc_admin_order_product_lookup';
/**
* Order by setting used for sorting categories data.
*
* @var string
*/
private $order_by = '';
/**
* Order setting used for sorting categories data.
*
* @var string
*/
private $order = '';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'category_id' => 'intval',
'items_sold' => 'intval',
'gross_revenue' => 'floatval',
'orders_count' => 'intval',
'products_count' => 'intval',
);
/**
* SQL columns to select in the db query.
*
* @var array
*/
protected $report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'gross_revenue' => 'SUM(product_gross_revenue) AS gross_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
// 'products_count' is not a SQL column at the moment, see below.
);
/**
* Return the database query with parameters used for Categories report: time span and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$sql_query_params = $this->get_time_period_sql_params( $query_args );
// Limit is left out here so that the grouping in code by PHP can be applied correctly.
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$allowed_products = $this->get_allowed_products( $query_args );
if ( count( $allowed_products ) > 0 ) {
$allowed_products_str = implode( ',', $allowed_products );
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$allowed_products_str})";
}
if ( is_array( $query_args['order_status'] ) && count( $query_args['order_status'] ) > 0 ) {
$statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['order_status'] );
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}posts ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}posts.ID";
$sql_query_params['where_clause'] .= " AND {$wpdb->prefix}posts.post_status IN ( '" . implode( "','", $statuses ) . "' ) ";
}
return $sql_query_params;
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'date_created';
}
return $order_by;
}
/**
* Compares values in 2 arrays based on criterion specified when called via sort_records.
*
* @param array $a Array 1 to compare.
* @param array $b Array 2 to compare.
* @return integer|WP_Error
*/
private function sort_callback( $a, $b ) {
if ( '' === $this->order_by || '' === $this->order ) {
return new WP_Error( 'woocommerce_reports_categories_sort_failed', __( 'Sorry, fetching categories data failed.', 'woocommerce' ) );
}
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
return 0;
} elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? -1 : 1;
} elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? 1 : -1;
}
}
/**
* Sorts the data based on given sorting criterion and order.
*
* @param array $data Data to sort.
* @param string $sort_by Sorting criterion, any of the column_types keys.
* @param string $direction Sorting direction: asc/desc.
*/
protected function sort_records( &$data, $sort_by, $direction ) {
$this->order_by = $this->normalize_order_by( $sort_by );
$this->order = $direction;
usort( $data, array( $this, 'sort_callback' ) );
}
/**
* Returns the page of data according to page number and items per page.
*
* @param array $data Data to paginate.
* @param integer $page_no Page number.
* @param integer $items_per_page Number of items per page.
* @return array
*/
protected function page_records( $data, $page_no, $items_per_page ) {
$offset = ( $page_no - 1 ) * $items_per_page;
return array_slice( $data, $offset, $items_per_page );
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'fields' => '*',
'categories' => array(),
// This is not a parameter for products reports per se, but maybe we should restricts order statuses here, too?
'order_status' => parent::get_report_order_statuses(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
if ( false === $data ) {
$data = (object) array(
'data' => array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$products_data = $wpdb->get_results(
"SELECT
product_id,
date_created,
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_clause']}
GROUP BY
product_id
", ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $products_data ) {
return new WP_Error( 'woocommerce_reports_categories_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ), array( 'status' => 500 ) );
}
// Group by category without a helper table, worst case we add it and change the SQL afterwards.
// Other option would be a join with wp_post and taxonomies, but a) performance might be bad, b) how to handle custom product tables?
$categories_data = array();
foreach ( $products_data as $product_data ) {
$categories = get_the_terms( $product_data['product_id'], 'product_cat' );
foreach ( $categories as $category ) {
$cat_id = $category->term_id;
if ( ! key_exists( $cat_id, $categories_data ) ) {
$categories_data[ $cat_id ] = array(
'category_id' => 0,
'items_sold' => 0,
'gross_revenue' => 0.0,
'orders_count' => 0,
'products_count' => 0,
);
}
$categories_data[ $cat_id ]['category_id'] = $cat_id;
$categories_data[ $cat_id ]['items_sold'] += $product_data['items_sold'];
$categories_data[ $cat_id ]['gross_revenue'] += $product_data['gross_revenue'];
$categories_data[ $cat_id ]['orders_count'] += $product_data['orders_count'];
$categories_data[ $cat_id ]['products_count'] ++;
}
}
$record_count = count( $categories_data );
$total_pages = (int) ceil( $record_count / $query_args['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->sort_records( $categories_data, $query_args['orderby'], $query_args['order'] );
$categories_data = $this->page_records( $categories_data, $query_args['page'], $query_args['per_page'] );
$categories_data = array_map( array( $this, 'cast_numbers' ), $categories_data );
$data = (object) array(
'data' => $categories_data,
'total' => $record_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
wp_cache_set( $cache_key, $data, $this->cache_group );
}
return $data;
}
}

View File

@ -0,0 +1,579 @@
<?php
/**
* WC_Admin_Reports_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC_Admin_Reports_Data_Store: Common parent for custom report data stores.
*/
class WC_Admin_Reports_Data_Store {
/**
* Cache group for the reports.
*
* @var string
*/
protected $cache_group = 'reports';
/**
* Time out for the cache.
*
* @var int
*/
protected $cache_timeout = 3600;
/**
* Table used as a data store for this report.
*
* @var string
*/
const TABLE_NAME = '';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array();
/**
* SQL columns to select in the db query.
*
* @var array
*/
protected $report_columns = array();
// TODO: this does not really belong here, maybe factor out the comparison as separate class?
/**
* Order by property, used in the cmp function.
*
* @var string
*/
private $order_by = '';
/**
* Order property, used in the cmp function.
*
* @var string
*/
private $order = '';
/**
* 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 interval_cmp( $a, $b ) {
if ( '' === $this->order_by || '' === $this->order ) {
return 0;
// TODO: should return WP_Error here perhaps?
}
if ( $a[ $this->order_by ] === $b[ $this->order_by ] ) {
return 0;
} elseif ( $a[ $this->order_by ] > $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? -1 : 1;
} elseif ( $a[ $this->order_by ] < $b[ $this->order_by ] ) {
return strtolower( $this->order ) === 'desc' ? 1 : -1;
}
}
/**
* Sorts intervals according to user's request.
*
* They are pre-sorted in SQL, but after adding gaps, they need to be sorted including the added ones.
*
* @param stdClass $data Data object, must contain an array under $data->intervals.
* @param string $sort_by Ordering property.
* @param string $direction DESC/ASC.
*/
protected function sort_intervals( &$data, $sort_by, $direction ) {
$this->order_by = $this->normalize_order_by( $sort_by );
$this->order = $direction;
usort( $data->intervals, array( $this, 'interval_cmp' ) );
}
/**
* Fills in interval gaps from DB with 0-filled objects.
*
* @param array $db_intervals Array of all intervals present in the db.
* @param DateTime $datetime_start Start date.
* @param DateTime $datetime_end End date.
* @param string $time_interval Time interval, e.g. day, week, month.
* @param stdClass $data Data with SQL extracted intervals.
* @return stdClass
*/
protected function fill_in_missing_intervals( $db_intervals, $datetime_start, $datetime_end, $time_interval, &$data ) {
// 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' ) );
$db_intervals = array_flip( $db_intervals );
$datetime = new DateTime( $datetime_start );
// Totals object used to get all needed properties.
$totals_arr = get_object_vars( $data->totals );
foreach ( $totals_arr as $key => $val ) {
$totals_arr[ $key ] = 0;
}
while ( $datetime <= $end_datetime ) {
$next_start = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval );
$time_id = WC_Admin_Reports_Interval::time_interval_id( $time_interval, $datetime );
// Either create fill-zero interval or use data from db.
if ( $next_start > $end_datetime ) {
$interval_end = $end_datetime->format( 'Y-m-d H:i:s' );
} else {
$prev_end_timestamp = (int) $next_start->format( 'U' ) - 1;
$prev_end = new DateTime();
$prev_end->setTimestamp( $prev_end_timestamp );
$interval_end = $prev_end->format( 'Y-m-d H:i:s' );
}
if ( array_key_exists( $time_id, $time_ids ) ) {
// For interval present in the db for this time frame, just fill in dates.
$record = &$data->intervals[ $time_ids[ $time_id ] ];
$record['date_start'] = $datetime->format( 'Y-m-d H:i:s' );
$record['date_end'] = $interval_end;
} elseif ( array_key_exists( $time_id, $db_intervals ) ) {
// For intervals present in the db outside of this time frame, do nothing.
} else {
// For intervals not present in the db, fabricate it.
$record_arr = array();
$record_arr['time_interval'] = $time_id;
$record_arr['date_start'] = $datetime->format( 'Y-m-d H:i:s' );
$record_arr['date_end'] = $interval_end;
$data->intervals[] = array_merge( $record_arr, $totals_arr );
}
$datetime = $next_start;
}
return $data;
}
/**
* Removes extra records from intervals so that only requested number of records get returned.
*
* @param stdClass $data Data from whose intervals the records get removed.
* @param int $page_no Offset requested by the user.
* @param int $items_per_page Number of records requested by the user.
* @param int $db_interval_count
* @param int $expected_interval_count
* @param string $order_by
*/
protected function remove_extra_records( &$data, $page_no, $items_per_page, $db_interval_count, $expected_interval_count, $order_by ) {
if ( 'date' === strtolower( $order_by ) ) {
$offset = 0;
} else {
$offset = ( $page_no - 1 ) * $items_per_page - $db_interval_count;
$offset = $offset < 0 ? 0 : $offset;
}
$count = $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
if ( $count < 0 ) {
$count = 0;
} elseif ( $count > $items_per_page ) {
$count = $items_per_page;
}
$data->intervals = array_slice( $data->intervals, $offset, $count );
}
/**
* Updates the LIMIT query part for Intervals query of the report.
*
* 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 int $db_records Number of records in the db for requested time period.
*/
protected function update_intervals_sql_params( &$intervals_query, &$query_args, $db_interval_count, $expected_interval_count ) {
if ( $db_interval_count === $expected_interval_count ) {
return;
}
if ( 'date' === strtolower( $query_args['orderby'] ) ) {
// page X in request translates to slightly different dates in the db, in case some
// records are missing from the db.
$start_iteration = 0;
$end_iteration = 0;
if ( 'asc' === strtolower( $query_args['order'] ) ) {
// ORDER BY date ASC.
$new_start_date = new DateTime( $query_args['after'] );
$intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page'];
$latest_end_date = new DateTime( $query_args['before'] );
for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
if ( $new_start_date > $latest_end_date ) {
$new_start_date = $latest_end_date;
$start_iteration = 0;
break;
}
$new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'] );
$start_iteration ++;
}
$new_end_date = clone $new_start_date;
for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) {
if ( $new_end_date > $latest_end_date ) {
$new_end_date = $latest_end_date;
$end_iteration = 0;
break;
}
$new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'] );
$end_iteration ++;
}
if ( $end_iteration ) {
$new_end_date_timestamp = (int) $new_end_date->format( 'U' ) - 1;
$new_end_date->setTimestamp( $new_end_date_timestamp );
}
} else {
// ORDER BY date DESC.
$new_end_date = new DateTime( $query_args['before'] );
$intervals_to_skip = ( $query_args['page'] - 1 ) * $intervals_query['per_page'];
$earliest_start_date = new DateTime( $query_args['after'] );
for ( $i = 0; $i < $intervals_to_skip; $i++ ) {
if ( $new_end_date < $earliest_start_date ) {
$new_end_date = $earliest_start_date;
$end_iteration = 0;
break;
}
$new_end_date = WC_Admin_Reports_Interval::iterate( $new_end_date, $query_args['interval'], true );
$end_iteration ++;
}
$new_start_date = clone $new_end_date;
for ( $i = 0; $i < $intervals_query['per_page']; $i++ ) {
if ( $new_start_date < $earliest_start_date ) {
$new_start_date = $earliest_start_date;
$start_iteration = 0;
break;
}
$new_start_date = WC_Admin_Reports_Interval::iterate( $new_start_date, $query_args['interval'], true );
$start_iteration ++;
}
if ( $start_iteration ) {
// 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 );
}
}
$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_clause'] = '';
$intervals_query['where_clause'] .= " AND date_created <= '{$query_args['adj_before']}'";
$intervals_query['where_clause'] .= " AND date_created >= '{$query_args['adj_after']}'";
$intervals_query['limit'] = 'LIMIT 0,' . $intervals_query['per_page'];
} else {
if ( 'asc' === $query_args['order'] ) {
$offset = ( ( $query_args['page'] - 1 ) * $intervals_query['per_page'] ) - ( $expected_interval_count - $db_interval_count );
$offset = $offset < 0 ? 0 : $offset;
$count = $query_args['page'] * $intervals_query['per_page'] - ( $expected_interval_count - $db_interval_count );
if ( $count < 0 ) {
$count = 0;
} elseif ( $count > $intervals_query['per_page'] ) {
$count = $intervals_query['per_page'];
}
$intervals_query['limit'] = 'LIMIT ' . $offset . ',' . $count;
}
// Otherwise no change in limit clause.
$query_args['adj_after'] = $query_args['after'];
$query_args['adj_before'] = $query_args['before'];
}
}
/**
* Returns string to be used as cache key for the data.
*
* @param array $params Query parameters.
* @return string
*/
protected function get_cache_key( $params ) {
// TODO: this is not working in PHP 5.2 (but revenue class has static methods, so it cannot use object property).
return 'woocommerce_' . $this::TABLE_NAME . '_' . md5( wp_json_encode( $params ) );
}
/**
* Casts strings returned from the database to appropriate data types for output.
*
* @param array $array Associative array of values extracted from the database.
* @return array|WP_Error
*/
protected function cast_numbers( $array ) {
$retyped_array = array();
$column_types = apply_filters( 'woocommerce_rest_reports_column_types', $this->column_types, $array );
foreach ( $array as $column_name => $value ) {
if ( isset( $column_types[ $column_name ] ) ) {
$retyped_array[ $column_name ] = $column_types[ $column_name ]( $value );
} else {
$retyped_array[ $column_name ] = $value;
}
}
return $retyped_array;
}
/**
* Returns a list of columns selected by the query_args formatted as a comma separated string.
*
* @param array $query_args User-supplied options.
* @return string
*/
protected function selected_columns( $query_args ) {
$selections = $this->report_columns;
if ( isset( $query_args['fields'] ) && is_array( $query_args['fields'] ) ) {
$keep = array();
foreach ( $query_args['fields'] as $field ) {
if ( isset( $selections[ $field ] ) ) {
$keep[ $field ] = $selections[ $field ];
}
}
$selections = implode( ', ', $keep );
} else {
$selections = implode( ', ', $selections );
}
return $selections;
}
/**
* Get the order statuses used when calculating reports.
*
* @return array
*/
protected static function get_report_order_statuses() {
return apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) );
}
/**
* Maps order status provided by the user to the one used in the database.
*
* @param string $status Order status.
* @return string
*/
protected function normalize_order_status( $status ) {
$status = trim( $status );
return 'wc-' . $status;
}
/**
* Normalizes order_by clause to match to SQL query.
*
* @param string $order_by Order by option requeste by user.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'time_interval';
}
return $order_by;
}
/**
* Updates start and end dates for intervals so that they represent intervals' borders, not times when data in db were recorded.
*
* E.g. if there are db records for only Tuesday and Thursday this week, the actual week interval is [Mon, Sun], not [Tue, Thu].
*
* @param DateTime $datetime_start Start date.
* @param DateTime $datetime_end End date.
* @param string $time_interval Time interval, e.g. day, week, month.
* @param array $intervals Array of intervals extracted from SQL db.
*/
protected function update_interval_boundary_dates( $datetime_start, $datetime_end, $time_interval, &$intervals ) {
foreach ( $intervals as $key => $interval ) {
$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.
$prev_start_timestamp = (int) $prev_start->format( 'U' ) + 1;
$prev_start->setTimestamp( $prev_start_timestamp );
if ( $datetime_start ) {
$start_datetime = new DateTime( $datetime_start );
$intervals[ $key ]['date_start'] = ( $prev_start < $start_datetime ? $start_datetime : $prev_start )->format( 'Y-m-d H:i:s' );
} else {
$intervals[ $key ]['date_start'] = $prev_start->format( 'Y-m-d H:i:s' );
}
$next_end = WC_Admin_Reports_Interval::iterate( $datetime, $time_interval );
$next_end_timestamp = (int) $next_end->format( 'U' ) - 1;
$next_end->setTimestamp( $next_end_timestamp );
if ( $datetime_end ) {
$end_datetime = new DateTime( $datetime_end );
$intervals[ $key ]['date_end'] = ( $next_end > $end_datetime ? $end_datetime : $next_end )->format( 'Y-m-d H:i:s' );
} else {
$intervals[ $key ]['date_end'] = $next_end->format( 'Y-m-d H:i:s' );
}
$intervals[ $key ]['interval'] = $time_interval;
}
}
/**
* Change structure of intervals to form a correct response.
*
* @param array $intervals Time interval, e.g. day, week, month.
*/
protected function create_interval_subtotals( &$intervals ) {
foreach ( $intervals as $key => $interval ) {
// Move intervals result to subtotals object.
$intervals[ $key ] = array(
'interval' => $interval['time_interval'],
'date_start' => $interval['date_start'],
'date_start_gmt' => $interval['date_start'],
'date_end' => $interval['date_end'],
'date_end_gmt' => $interval['date_end'],
);
unset( $interval['interval'] );
unset( $interval['date_start'] );
unset( $interval['date_end'] );
unset( $interval['datetime_anchor'] );
unset( $interval['time_interval'] );
$intervals[ $key ]['subtotals'] = (object) $this->cast_numbers( $interval );
}
}
/**
* Fills WHERE clause of SQL request for 'Totals' section of data response based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_time_period_sql_params( $query_args ) {
$sql_query = array(
'from_clause' => '',
'where_clause' => '',
);
if ( isset( $query_args['before'] ) && '' !== $query_args['before'] ) {
$datetime = new DateTime( $query_args['before'] );
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$sql_query['where_clause'] .= " AND date_created <= '$datetime_str'";
}
if ( isset( $query_args['after'] ) && '' !== $query_args['after'] ) {
$datetime = new DateTime( $query_args['after'] );
$datetime_str = $datetime->format( WC_Admin_Reports_Interval::$sql_datetime_format );
$sql_query['where_clause'] .= " AND date_created >= '$datetime_str'";
}
return $sql_query;
}
/**
* Fills LIMIT clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_limit_sql_params( $query_args ) {
$sql_query['per_page'] = get_option( 'posts_per_page' );
if ( isset( $query_args['per_page'] ) && is_numeric( $query_args['per_page'] ) ) {
$sql_query['per_page'] = (int) $query_args['per_page'];
}
$sql_query['offset'] = 0;
if ( isset( $query_args['page'] ) ) {
$sql_query['offset'] = ( (int) $query_args['page'] - 1 ) * $sql_query['per_page'];
}
$sql_query['limit'] = "LIMIT {$sql_query['offset']}, {$sql_query['per_page']}";
return $sql_query;
}
/**
* Fills ORDER BY clause of SQL request based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_order_by_sql_params( $query_args ) {
$sql_query['order_by_clause'] = '';
if ( isset( $query_args['orderby'] ) ) {
$sql_query['order_by_clause'] = $this->normalize_order_by( $query_args['orderby'] );
}
if ( isset( $query_args['order'] ) ) {
$sql_query['order_by_clause'] .= ' ' . $query_args['order'];
} else {
$sql_query['order_by_clause'] .= ' DESC';
}
return $sql_query;
}
/**
* Fills FROM and WHERE clauses of SQL request for 'Intervals' section of data response based on user supplied parameters.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_intervals_sql_params( $query_args ) {
$intervals_query = array(
'from_clause' => '',
'where_clause' => '',
);
$intervals_query = array_merge( $intervals_query, $this->get_time_period_sql_params( $query_args ) );
if ( isset( $query_args['interval'] ) && '' !== $query_args['interval'] ) {
$interval = $query_args['interval'];
$intervals_query['select_clause'] = WC_Admin_Reports_Interval::mysql_datetime_format( $interval );
}
$intervals_query = array_merge( $intervals_query, $this->get_limit_sql_params( $query_args ) );
$intervals_query = array_merge( $intervals_query, $this->get_order_by_sql_params( $query_args ) );
return $intervals_query;
}
/**
* Returns an array of products belonging to given categories.
*
* @param array $categories List of categories IDs.
* @return array|stdClass
*/
protected function get_products_by_cat_ids( $categories ) {
$product_categories = get_categories( array(
'hide_empty' => 0,
'taxonomy' => 'product_cat',
) );
$cat_slugs = array();
$categories = array_flip( $categories );
foreach ( $product_categories as $product_cat ) {
if ( key_exists( $product_cat->cat_ID, $categories ) ) {
$cat_slugs[] = $product_cat->slug;
}
}
$args = array(
'category' => $cat_slugs,
'limit' => -1,
);
return wc_get_products( $args );
}
/**
* Returns ids of allowed products, based on query arguments from the user.
*
* @param array $query_args Parameters supplied by the user.
* @return array
*/
protected function get_allowed_products( $query_args ) {
$allowed_products = array();
if ( isset( $query_args['categories'] ) && is_array( $query_args['categories'] ) && count( $query_args['categories'] ) > 0 ) {
$allowed_products = $this->get_products_by_cat_ids( $query_args['categories'] );
$allowed_products = wc_list_pluck( $allowed_products, 'get_id' );
}
if ( isset( $query_args['products'] ) && is_array( $query_args['products'] ) && count( $query_args['products'] ) > 0 ) {
if ( count( $allowed_products ) > 0 ) {
$allowed_products = array_intersect( $allowed_products, $query_args['products'] );
} else {
$allowed_products = $query_args['products'];
}
}
return $allowed_products;
}
}

View File

@ -0,0 +1,463 @@
<?php
/**
* WC_Admin_Reports_Orders_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_Admin_Order_Stats_Background_Process', false ) ) {
include_once WC_ADMIN_ABSPATH . '/includes/class-wc-admin-order-stats-background-process.php';
}
/**
* WC_Admin_Reports_Orders_Data_Store.
*
* @version 3.5.0
*/
class WC_Admin_Reports_Orders_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Table used to get the data.
*
* @var string
*/
const TABLE_NAME = 'wc_admin_order_stats';
/**
* Cron event name.
*/
const CRON_EVENT = 'wc_admin_order_stats_update';
protected $column_types = array(
'orders_count' => 'intval',
'num_items_sold' => 'intval',
'gross_revenue' => 'floatval',
'coupons' => 'floatval',
'refunds' => 'floatval',
'taxes' => 'floatval',
'shipping' => 'floatval',
'net_revenue' => 'floatval',
'avg_items_per_order' => 'floatval',
'avg_order_value' => 'floatval',
);
protected $report_columns = array(
'orders_count' => 'COUNT(*) as orders_count',
'num_items_sold' => 'SUM(num_items_sold) as num_items_sold',
'gross_revenue' => 'SUM(gross_total) AS gross_revenue',
'coupons' => 'SUM(coupon_total) AS coupons',
'refunds' => 'SUM(refund_total) AS refunds',
'taxes' => 'SUM(tax_total) AS taxes',
'shipping' => 'SUM(shipping_total) AS shipping',
'net_revenue' => 'SUM(net_total) AS net_revenue',
'avg_items_per_order' => 'AVG(num_items_sold) AS avg_items_per_order',
'avg_order_value' => 'AVG(gross_total) AS avg_order_value',
);
/**
* Background process to populate order stats.
*
* @var WC_Admin_Order_Stats_Background_Process
*/
protected static $background_process;
/**
* Constructor.
*/
public function __construct() {
if ( ! self::$background_process ) {
self::$background_process = new WC_Admin_Order_Stats_Background_Process();
}
}
/**
* Set up all the hooks for maintaining and populating table data.
*/
public static function init() {
add_action( 'save_post', array( __CLASS__, 'sync_order' ) );
if ( ! self::$background_process ) {
self::$background_process = new WC_Admin_Order_Stats_Background_Process();
}
}
/**
* Returns expected number of items on the page in case of date ordering.
*
* @param $expected_interval_count
* @param $items_per_page
* @param $page_no
*
* @return float|int
*/
protected function expected_intervals_on_page( $expected_interval_count, $items_per_page, $page_no ) {
$total_pages = (int) ceil( $expected_interval_count / $items_per_page );
if ( $page_no < $total_pages ) {
return $items_per_page;
} elseif ( $page_no === $total_pages ) {
return $expected_interval_count - ( $page_no - 1 ) * $items_per_page;
} else {
return 0;
}
}
/**
* Returns true if there are any intervals that need to be filled in the response.
*
* @param $expected_interval_count
* @param $db_records
* @param $items_per_page
* @param $page_no
* @param $order
* @param $order_by
* @param $intervals_count
*
* @return bool
*/
protected function intervals_missing( $expected_interval_count, $db_records, $items_per_page, $page_no, $order, $order_by, $intervals_count ) {
if ( $expected_interval_count > $db_records ) {
if ( 'date' === $order_by ) {
$expected_intervals_on_page = $this->expected_intervals_on_page( $expected_interval_count, $items_per_page, $page_no );
if ( $intervals_count < $expected_intervals_on_page ) {
return true;
} else {
return false;
}
} else {
if ( 'desc' === $order ) {
if ( $page_no > floor( $db_records / $items_per_page ) ) {
return true;
} else {
return false;
}
} elseif ( 'asc' === $order ) {
if ( $page_no <= ceil( ( $expected_interval_count - $db_records ) / $items_per_page ) ) {
return true;
} else {
return false;
}
} else {
// Invalid ordering.
return false;
}
}
} else {
return false;
}
}
/**
* Updates the totals and intervals database queries with parameters used for Orders report: categories, coupons and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @param array $totals_query Array of options for totals db query.
* @param array $intervals_query Array of options for intervals db query.
*/
protected function orders_stats_sql_filter( $query_args, &$totals_query, &$intervals_query ) {
// TODO: performance of all of this?
global $wpdb;
$where_clause = '';
$from_clause = '';
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
$allowed_products = $this->get_allowed_products( $query_args );
if ( count( $allowed_products ) > 0 ) {
$allowed_products_str = implode( ',', $allowed_products );
$where_clause .= " AND {$orders_stats_table}.order_id IN (
SELECT
DISTINCT {$wpdb->prefix}wc_admin_order_product_lookup.order_id
FROM
{$wpdb->prefix}wc_admin_order_product_lookup
WHERE
{$wpdb->prefix}wc_admin_order_product_lookup.product_id IN ({$allowed_products_str})
)";
}
if ( is_array( $query_args['coupons'] ) && count( $query_args['coupons'] ) > 0 ) {
$allowed_coupons_str = implode( ', ', $query_args['coupons'] );
$where_clause .= " AND {$orders_stats_table}.order_id IN (
SELECT
DISTINCT {$wpdb->prefix}wc_order_coupon_lookup.order_id
FROM
{$wpdb->prefix}wc_order_coupon_lookup
WHERE
{$wpdb->prefix}wc_order_coupon_lookup.coupon_id IN ({$allowed_coupons_str})
)";
}
if ( is_array( $query_args['order_status'] ) && count( $query_args['order_status'] ) > 0 ) {
$statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['order_status'] );
$from_clause .= " JOIN {$wpdb->prefix}posts ON {$orders_stats_table}.order_id = {$wpdb->prefix}posts.ID";
$where_clause .= " AND {$wpdb->prefix}posts.post_status IN ( '" . implode( "','", $statuses ) . "' ) ";
}
// To avoid requesting the subqueries twice, the result is applied to all queries passed to the method.
$totals_query['where_clause'] .= $where_clause;
$totals_query['from_clause'] .= $from_clause;
$intervals_query['where_clause'] .= $where_clause;
$intervals_query['from_clause'] .= $from_clause;
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @since 3.5.0
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only applied when not using REST API, as the API has its own defaults that overwrite these for most values (except before, after, etc).
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'interval' => 'week',
'fields' => '*',
'categories' => array(),
'coupons' => array(),
'order_status' => parent::get_report_order_statuses(),
'products' => array(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
if ( false === $data ) {
$data = (object) array(
'totals' => (object) array(),
'intervals' => (object) array(),
'total' => 0,
'pages' => 0,
'page_no' => 0,
);
$selections = $this->selected_columns( $query_args );
$totals_query = $this->get_time_period_sql_params( $query_args );
$intervals_query = $this->get_intervals_sql_params( $query_args );
// Additional filtering for Orders report.
$this->orders_stats_sql_filter( $query_args, $totals_query, $intervals_query );
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_clause']}", ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
// Specification says these are not included in totals.
unset( $totals[0]['date_start'] );
unset( $totals[0]['date_end'] );
$totals = (object) $this->cast_numbers( $totals[0] );
$db_intervals = $wpdb->get_col(
"SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_clause']}
GROUP BY
time_interval"
); // WPCS: cache ok, DB call ok, , unprepared SQL ok.
$db_interval_count = count( $db_intervals );
$expected_interval_count = WC_Admin_Reports_Interval::intervals_between( $query_args['after'], $query_args['before'], $query_args['interval'] );
$total_pages = (int) ceil( $expected_interval_count / $intervals_query['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return $data;
}
$this->update_intervals_sql_params( $intervals_query, $query_args, $db_interval_count, $expected_interval_count );
if ( '' !== $selections ) {
$selections = ', ' . $selections;
}
$intervals = $wpdb->get_results(
"SELECT
MAX(date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}", ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new WP_Error( 'woocommerce_reports_revenue_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $expected_interval_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
if ( $this->intervals_missing( $expected_interval_count, $db_interval_count, $intervals_query['per_page'], $query_args['page'], $query_args['order'], $query_args['orderby'], count( $intervals ) ) ) {
$this->fill_in_missing_intervals( $db_intervals, $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data );
$this->sort_intervals( $data, $query_args['orderby'], $query_args['order'] );
$this->remove_extra_records( $data, $query_args['page'], $intervals_query['per_page'], $db_interval_count, $expected_interval_count, $query_args['orderby'] );
} else {
$this->update_interval_boundary_dates( $query_args['adj_after'], $query_args['adj_before'], $query_args['interval'], $data->intervals );
}
$this->create_interval_subtotals( $data->intervals );
wp_cache_set( $cache_key, $data, $this->cache_group );
}
return $data;
}
/**
* Queue a background process that will repopulate the entire orders stats database.
*
* @todo Make this work on large DBs.
*/
public static function queue_order_stats_repopulate_database() {
// This needs to be updated to work in batches instead of getting all orders, as
// that will not work well on DBs with more than a few hundred orders.
$order_ids = wc_get_orders( array(
'limit' => -1,
'status' => parent::get_report_order_statuses(),
'type' => 'shop_order',
'return' => 'ids',
) );
foreach ( $order_ids as $id ) {
self::$background_process->push_to_queue( $id );
}
self::$background_process->save();
self::$background_process->dispatch();
}
/**
* Add order information to the lookup table when orders are created or modified.
*
* @param int $post_id Post ID.
*/
public static function sync_order( $post_id ) {
if ( 'shop_order' !== get_post_type( $post_id ) ) {
return;
}
$order = wc_get_order( $post_id );
if ( ! $order ) {
return;
}
self::update( $order );
}
/**
* Update the database with stats data.
*
* @param WC_Order $order Order to update row for.
* @return int|bool Number or rows modified or false on failure.
*/
public static function update( $order ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
if ( ! $order->get_id() || ! $order->get_date_created() ) {
return false;
}
if ( ! in_array( $order->get_status(), parent::get_report_order_statuses(), true ) ) {
$wpdb->delete( $table_name, array(
'order_id' => $order->get_id(),
) );
return;
}
$data = array(
'order_id' => $order->get_id(),
'date_created' => $order->get_date_created()->date( 'Y-m-d H:i:s' ),
'num_items_sold' => self::get_num_items_sold( $order ),
'gross_total' => $order->get_total(),
'coupon_total' => $order->get_total_discount(),
'refund_total' => $order->get_total_refunded(),
'tax_total' => $order->get_total_tax(),
'shipping_total' => $order->get_shipping_total(),
'net_total' => $order->get_total() - $order->get_total_tax() - $order->get_shipping_total(),
);
// Update or add the information to the DB.
return $wpdb->replace(
$table_name,
$data,
array(
'%d',
'%s',
'%d',
'%f',
'%f',
'%f',
'%f',
'%f',
'%f',
)
);
}
/**
* Calculation methods.
*/
/**
* Get number of items sold among all orders.
*
* @param array $order WC_Order object.
* @return int
*/
protected static function get_num_items_sold( $order ) {
$num_items = 0;
$line_items = $order->get_items( 'line_item' );
foreach ( $line_items as $line_item ) {
$num_items += $line_item->get_quantity();
}
return $num_items;
}
}

View File

@ -0,0 +1,228 @@
<?php
/**
* WC_Admin_Reports_Products_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Reports_Products_Data_Store.
*
*/
class WC_Admin_Reports_Products_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Table used to get the data.
*
* @var string
*/
const TABLE_NAME = 'wc_admin_order_product_lookup';
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'date_start' => 'strval',
'date_end' => 'strval',
'product_id' => 'intval',
'items_sold' => 'intval',
'gross_revenue' => 'floatval',
'orders_count' => 'intval',
// Extended attributes.
'name' => 'strval',
'price' => 'floatval',
'image' => 'strval',
'permalink' => 'strval',
);
/**
* SQL columns to select in the db query and their mapping to SQL code.
*
* @var array
*/
protected $report_columns = array(
'product_id' => 'product_id',
'items_sold' => 'SUM(product_qty) as items_sold',
'gross_revenue' => 'SUM(product_gross_revenue) AS gross_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
);
/**
* Extended product attributes to include in the data.
*
* @var array
*/
protected $extended_attributes = array(
'name',
'price',
'image',
'permalink',
);
/**
* Updates the database query with parameters used for Products report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @return array Array of parameters used for SQL query.
*/
protected function get_sql_query_params( $query_args ) {
global $wpdb;
$sql_query_params = $this->get_time_period_sql_params( $query_args );
$sql_query_params = array_merge( $sql_query_params, $this->get_limit_sql_params( $query_args ) );
$sql_query_params = array_merge( $sql_query_params, $this->get_order_by_sql_params( $query_args ) );
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
$allowed_products = $this->get_allowed_products( $query_args );
if ( count( $allowed_products ) > 0 ) {
$allowed_products_str = implode( ',', $allowed_products );
$sql_query_params['where_clause'] .= " AND {$order_product_lookup_table}.product_id IN ({$allowed_products_str})";
}
if ( is_array( $query_args['order_status'] ) && count( $query_args['order_status'] ) > 0 ) {
$statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['order_status'] );
$sql_query_params['from_clause'] .= " JOIN {$wpdb->prefix}posts ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}posts.ID";
$sql_query_params['where_clause'] .= " AND {$wpdb->prefix}posts.post_status IN ( '" . implode( "','", $statuses ) . "' ) ";
}
return $sql_query_params;
}
/**
* Maps ordering specified by the user to columns in the database/fields in the data.
*
* @param string $order_by Sorting criterion.
* @return string
*/
protected function normalize_order_by( $order_by ) {
if ( 'date' === $order_by ) {
return 'date_created';
}
return $order_by;
}
/**
* Enriches the product data with attributes specified by the extended_attributes.
*
* @param array $products_data Product data.
*/
protected function include_extended_product_info( &$products_data ) {
foreach ( $products_data as $key => $product_data ) {
$product = wc_get_product( $product_data['product_id'] );
$extended_attributes = apply_filters( 'woocommerce_rest_reports_products_extended_attributes', $this->extended_attributes, $product_data );
foreach ( $extended_attributes as $extended_attribute ) {
$function = 'get_' . $extended_attribute;
if ( is_callable( array( $product, $function ) ) ) {
$value = $product->{$function}();
$products_data[ $key ][ $extended_attribute ] = $value;
}
}
}
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'fields' => '*',
'categories' => array(),
'products' => array(),
'extended_product_info' => false,
// This is not a parameter for products reports per se, but we want to only take into account selected order types.
'order_status' => parent::get_report_order_statuses(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$data = wp_cache_get( $cache_key, $this->cache_group );
if ( false === $data ) {
$selections = $this->selected_columns( $query_args );
$sql_query_params = $this->get_sql_query_params( $query_args );
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
product_id
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_clause']}
GROUP BY
product_id
) AS tt"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_pages = (int) ceil( $db_records_count / $sql_query_params['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$product_data = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$sql_query_params['from_clause']}
WHERE
1=1
{$sql_query_params['where_clause']}
GROUP BY
product_id
ORDER BY
{$sql_query_params['order_by_clause']}
{$sql_query_params['limit']}
", ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $product_data ) {
return new WP_Error( 'woocommerce_reports_products_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
if ( $query_args['extended_product_info'] ) {
$this->include_extended_product_info( $product_data );
}
$product_data = array_map( array( $this, 'cast_numbers' ), $product_data );
$data = (object) array(
'data' => $product_data,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
wp_cache_set( $cache_key, $data, $this->cache_group );
}
return $data;
}
}

View File

@ -0,0 +1,200 @@
<?php
/**
* WC_Admin_Reports_Products_Stats_Data_Store class file.
*
* @package WooCommerce Admin/Classes
*/
defined( 'ABSPATH' ) || exit;
/**
* WC_Reports_Products_Stats_Data_Store.
*
*/
class WC_Admin_Reports_Products_Stats_Data_Store extends WC_Admin_Reports_Products_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
/**
* Mapping columns to data type to return correct response types.
*
* @var array
*/
protected $column_types = array(
'date_start' => 'strval',
'date_end' => 'strval',
'product_id' => 'intval',
'items_sold' => 'intval',
'gross_revenue' => 'floatval',
'orders_count' => 'intval',
'products_count' => 'intval',
);
/**
* SQL columns to select in the db query.
*
* @var array
*/
protected $report_columns = array(
'items_sold' => 'SUM(product_qty) as items_sold',
'gross_revenue' => 'SUM(product_gross_revenue) AS gross_revenue',
'orders_count' => 'COUNT(DISTINCT order_id) as orders_count',
'products_count' => 'COUNT(DISTINCT product_id) as products_count',
);
/**
* Updates the database query with parameters used for Products Stats report: categories and order status.
*
* @param array $query_args Query arguments supplied by the user.
* @param array $totals_params SQL parameters for the totals query.
* @param array $intervals_params SQL parameters for the intervals query.
*/
protected function update_sql_query_params( $query_args, &$totals_params, &$intervals_params ) {
global $wpdb;
$allowed_products = $this->get_allowed_products( $query_args );
$products_where_clause = '';
$products_from_clause = '';
$order_product_lookup_table = $wpdb->prefix . self::TABLE_NAME;
if ( count( $allowed_products ) > 0 ) {
$allowed_products_str = implode( ',', $allowed_products );
$products_where_clause .= " AND {$order_product_lookup_table}.product_id IN ({$allowed_products_str})";
}
if ( is_array( $query_args['order_status'] ) && count( $query_args['order_status'] ) > 0 ) {
$statuses = array_map( array( $this, 'normalize_order_status' ), $query_args['order_status'] );
$products_from_clause .= " JOIN {$wpdb->prefix}posts ON {$order_product_lookup_table}.order_id = {$wpdb->prefix}posts.ID";
$products_where_clause .= " AND {$wpdb->prefix}posts.post_status IN ( '" . implode( "','", $statuses ) . "' ) ";
}
$totals_params = array_merge( $totals_params, $this->get_time_period_sql_params( $query_args ) );
$totals_params['where_clause'] .= $products_where_clause;
$totals_params['from_clause'] .= $products_from_clause;
$intervals_params = array_merge( $intervals_params, $this->get_intervals_sql_params( $query_args ) );
$intervals_params['where_clause'] .= $products_where_clause;
$intervals_params['from_clause'] .= $products_from_clause;
}
/**
* Returns the report data based on parameters supplied by the user.
*
* @since 3.5.0
* @param array $query_args Query parameters.
* @return stdClass|WP_Error Data.
*/
public function get_data( $query_args ) {
global $wpdb;
$table_name = $wpdb->prefix . self::TABLE_NAME;
$now = time();
$week_back = $now - WEEK_IN_SECONDS;
// These defaults are only partially applied when used via REST API, as that has its own defaults.
$defaults = array(
'per_page' => get_option( 'posts_per_page' ),
'page' => 1,
'order' => 'DESC',
'orderby' => 'date',
'before' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $now ),
'after' => date( WC_Admin_Reports_Interval::$iso_datetime_format, $week_back ),
'fields' => '*',
'categories' => array(),
'interval' => 'week',
'products' => array(),
// This is not a parameter for products reports per se, but we should probably restricts order statuses here, too.
'order_status' => parent::get_report_order_statuses(),
);
$query_args = wp_parse_args( $query_args, $defaults );
$cache_key = $this->get_cache_key( $query_args );
$product_data = wp_cache_get( $cache_key, $this->cache_group );
if ( false === $product_data ) {
$selections = $this->selected_columns( $query_args );
$totals_query = array();
$intervals_query = array();
$this->update_sql_query_params( $query_args, $totals_query, $intervals_query );
$db_records_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM (
SELECT
{$intervals_query['select_clause']} AS time_interval
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_clause']}
GROUP BY
time_interval
) AS t"
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
$total_pages = (int) ceil( $db_records_count / $intervals_query['per_page'] );
if ( $query_args['page'] < 1 || $query_args['page'] > $total_pages ) {
return array();
}
$totals = $wpdb->get_results(
"SELECT
{$selections}
FROM
{$table_name}
{$totals_query['from_clause']}
WHERE
1=1
{$totals_query['where_clause']}", ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $totals ) {
return new WP_Error( 'woocommerce_reports_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
if ( '' !== $selections ) {
$selections = ', ' . $selections;
}
$intervals = $wpdb->get_results(
"SELECT
MAX(date_created) AS datetime_anchor,
{$intervals_query['select_clause']} AS time_interval
{$selections}
FROM
{$table_name}
{$intervals_query['from_clause']}
WHERE
1=1
{$intervals_query['where_clause']}
GROUP BY
time_interval
ORDER BY
{$intervals_query['order_by_clause']}
{$intervals_query['limit']}", ARRAY_A
); // WPCS: cache ok, DB call ok, unprepared SQL ok.
if ( null === $intervals ) {
return new WP_Error( 'woocommerce_reports_products_stats_result_failed', __( 'Sorry, fetching revenue data failed.', 'woocommerce' ) );
}
$totals = (object) $this->cast_numbers( $totals[0] );
$this->update_interval_boundary_dates( $query_args['after'], $query_args['before'], $query_args['interval'], $intervals );
$this->create_interval_subtotals( $intervals );
$data = (object) array(
'totals' => $totals,
'intervals' => $intervals,
'total' => $db_records_count,
'pages' => $total_pages,
'page_no' => (int) $query_args['page'],
);
wp_cache_set( $cache_key, $data, $this->cache_group );
}
return $data;
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Reports Data Store Interface
*
* @version 3.5.0
* @package WooCommerce Admin/Interface
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WooCommerce Reports data store interface.
*
* @since 3.5.0
*/
interface WC_Admin_Reports_Data_Store_Interface {
/**
* Get the data based on args.
*
* @param array $args Query parameters.
* @return stdClass|WP_Error
*/
public function get_data( $args );
}

View File

@ -0,0 +1,115 @@
<?php
/**
* Make an entry in the wc_admin_order_product_lookup table for an order.
*
* @since 3.5.0
* @param int $order_id Order ID.
* @return void
*/
function wc_admin_order_product_lookup_entry( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
foreach ( $order->get_items() as $order_item ) {
$wpdb->replace(
$wpdb->prefix . 'wc_admin_order_product_lookup',
array(
'order_item_id' => $order_item->get_id(),
'order_id' => $order->get_id(),
'product_id' => $order_item->get_product_id( 'edit' ),
'customer_id' => ( 0 < $order->get_customer_id( 'edit' ) ) ? $order->get_customer_id( 'edit' ) : null,
'product_qty' => $order_item->get_quantity( 'edit' ),
'product_gross_revenue' => $order_item->get_subtotal( 'edit' ),
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
),
array(
'%d',
'%d',
'%d',
'%d',
'%d',
'%f',
'%s',
)
);
}
}
add_action( 'save_post', 'wc_admin_order_product_lookup_entry', 10, 1 );
/**
* Make an entry in the wc_order_tax_lookup table for an order.
*
* @since 3.5.0
* @param int $order_id Order ID.
* @return void
*/
function wc_order_tax_lookup_entry( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
foreach ( $order->get_items( 'tax' ) as $tax_item ) {
$wpdb->replace(
$wpdb->prefix . 'wc_order_tax_lookup',
array(
'order_id' => $order->get_id(),
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
'tax_rate_id' => $tax_item->get_rate_id(),
'shipping_tax' => $tax_item->get_shipping_tax_total(),
'order_tax' => $tax_item->get_tax_total(),
'total_tax' => $tax_item->get_tax_total() + $tax_item->get_shipping_tax_total(),
),
array(
'%d',
'%s',
'%d',
'%f',
'%f',
'%f',
)
);
}
}
add_action( 'save_post', 'wc_order_tax_lookup_entry', 10, 1 );
/**
* Make an entry in the wc_order_coupon_lookup table for an order.
*
* @since 3.5.0
* @param int $order_id Order ID.
* @return void
*/
function wc_order_coupon_lookup_entry( $order_id ) {
global $wpdb;
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$coupon_items = $order->get_items( 'coupon' );
foreach ( $coupon_items as $coupon_item ) {
$wpdb->replace(
$wpdb->prefix . 'wc_order_coupon_lookup',
array(
'order_id' => $order_id,
'coupon_id' => $coupon_item->get_id(),
'coupon_gross_discount' => $coupon_item->get_discount(),
'date_created' => date( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
),
array(
'%d',
'%d',
'%f',
'%s',
)
);
}
}
add_action( 'save_post', 'wc_order_coupon_lookup_entry', 10, 1 );

View File

@ -16,6 +16,10 @@ if ( ! defined( 'WC_ADMIN_APP' ) ) {
define( 'WC_ADMIN_APP', 'wc-admin-app' );
}
if ( ! defined( 'WC_ADMIN_ABSPATH' ) ) {
define( 'WC_ADMIN_ABSPATH', dirname( __FILE__ ) );
}
/**
* Notify users of the plugin requirements
*/
@ -40,6 +44,9 @@ function wc_admin_plugins_loaded() {
return;
}
// Initialize the WC API extensions.
require_once dirname( __FILE__ ) . '/includes/class-wc-admin-api-init.php';
// Some common utilities
require_once dirname( __FILE__ ) . '/lib/common.php';