class-wc-rest-report-sales-controller.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. /**
  3. * REST API Reports controller
  4. *
  5. * Handles requests to the reports/sales endpoint.
  6. *
  7. * @author WooThemes
  8. * @category API
  9. * @package WooCommerce/API
  10. * @since 3.0.0
  11. */
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. exit;
  14. }
  15. /**
  16. * REST API Report Sales controller class.
  17. *
  18. * @package WooCommerce/API
  19. * @extends WC_REST_Controller
  20. */
  21. class WC_REST_Report_Sales_V1_Controller extends WC_REST_Controller {
  22. /**
  23. * Endpoint namespace.
  24. *
  25. * @var string
  26. */
  27. protected $namespace = 'wc/v1';
  28. /**
  29. * Route base.
  30. *
  31. * @var string
  32. */
  33. protected $rest_base = 'reports/sales';
  34. /**
  35. * Report instance.
  36. *
  37. * @var WC_Admin_Report
  38. */
  39. protected $report;
  40. /**
  41. * Register the routes for sales reports.
  42. */
  43. public function register_routes() {
  44. register_rest_route( $this->namespace, '/' . $this->rest_base, array(
  45. array(
  46. 'methods' => WP_REST_Server::READABLE,
  47. 'callback' => array( $this, 'get_items' ),
  48. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  49. 'args' => $this->get_collection_params(),
  50. ),
  51. 'schema' => array( $this, 'get_public_item_schema' ),
  52. ) );
  53. }
  54. /**
  55. * Check whether a given request has permission to read report.
  56. *
  57. * @param WP_REST_Request $request Full details about the request.
  58. * @return WP_Error|boolean
  59. */
  60. public function get_items_permissions_check( $request ) {
  61. if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) {
  62. return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
  63. }
  64. return true;
  65. }
  66. /**
  67. * Get sales reports.
  68. *
  69. * @param WP_REST_Request $request
  70. * @return array|WP_Error
  71. */
  72. public function get_items( $request ) {
  73. $data = array();
  74. $item = $this->prepare_item_for_response( null, $request );
  75. $data[] = $this->prepare_response_for_collection( $item );
  76. return rest_ensure_response( $data );
  77. }
  78. /**
  79. * Prepare a report sales object for serialization.
  80. *
  81. * @param null $_
  82. * @param WP_REST_Request $request Request object.
  83. * @return WP_REST_Response $response Response data.
  84. */
  85. public function prepare_item_for_response( $_, $request ) {
  86. // Set date filtering.
  87. $filter = array(
  88. 'period' => $request['period'],
  89. 'date_min' => $request['date_min'],
  90. 'date_max' => $request['date_max'],
  91. );
  92. $this->setup_report( $filter );
  93. // New customers.
  94. $users_query = new WP_User_Query(
  95. array(
  96. 'fields' => array( 'user_registered' ),
  97. 'role' => 'customer',
  98. )
  99. );
  100. $customers = $users_query->get_results();
  101. foreach ( $customers as $key => $customer ) {
  102. if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) {
  103. unset( $customers[ $key ] );
  104. }
  105. }
  106. $total_customers = count( $customers );
  107. $report_data = $this->report->get_report_data();
  108. $period_totals = array();
  109. // Setup period totals by ensuring each period in the interval has data.
  110. for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) {
  111. switch ( $this->report->chart_groupby ) {
  112. case 'day' :
  113. $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) );
  114. break;
  115. default :
  116. $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) );
  117. break;
  118. }
  119. // Set the customer signups for each period.
  120. $customer_count = 0;
  121. foreach ( $customers as $customer ) {
  122. if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) {
  123. $customer_count++;
  124. }
  125. }
  126. $period_totals[ $time ] = array(
  127. 'sales' => wc_format_decimal( 0.00, 2 ),
  128. 'orders' => 0,
  129. 'items' => 0,
  130. 'tax' => wc_format_decimal( 0.00, 2 ),
  131. 'shipping' => wc_format_decimal( 0.00, 2 ),
  132. 'discount' => wc_format_decimal( 0.00, 2 ),
  133. 'customers' => $customer_count,
  134. );
  135. }
  136. // add total sales, total order count, total tax and total shipping for each period
  137. foreach ( $report_data->orders as $order ) {
  138. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
  139. if ( ! isset( $period_totals[ $time ] ) ) {
  140. continue;
  141. }
  142. $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 );
  143. $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 );
  144. $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 );
  145. }
  146. foreach ( $report_data->order_counts as $order ) {
  147. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) );
  148. if ( ! isset( $period_totals[ $time ] ) ) {
  149. continue;
  150. }
  151. $period_totals[ $time ]['orders'] = (int) $order->count;
  152. }
  153. // Add total order items for each period.
  154. foreach ( $report_data->order_items as $order_item ) {
  155. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) );
  156. if ( ! isset( $period_totals[ $time ] ) ) {
  157. continue;
  158. }
  159. $period_totals[ $time ]['items'] = (int) $order_item->order_item_count;
  160. }
  161. // Add total discount for each period.
  162. foreach ( $report_data->coupons as $discount ) {
  163. $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) );
  164. if ( ! isset( $period_totals[ $time ] ) ) {
  165. continue;
  166. }
  167. $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 );
  168. }
  169. $sales_data = array(
  170. 'total_sales' => $report_data->total_sales,
  171. 'net_sales' => $report_data->net_sales,
  172. 'average_sales' => $report_data->average_sales,
  173. 'total_orders' => $report_data->total_orders,
  174. 'total_items' => $report_data->total_items,
  175. 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ),
  176. 'total_shipping' => $report_data->total_shipping,
  177. 'total_refunds' => $report_data->total_refunds,
  178. 'total_discount' => $report_data->total_coupons,
  179. 'totals_grouped_by' => $this->report->chart_groupby,
  180. 'totals' => $period_totals,
  181. 'total_customers' => $total_customers,
  182. );
  183. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  184. $data = $this->add_additional_fields_to_object( $sales_data, $request );
  185. $data = $this->filter_response_by_context( $data, $context );
  186. // Wrap the data in a response object.
  187. $response = rest_ensure_response( $data );
  188. $response->add_links( array(
  189. 'about' => array(
  190. 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ),
  191. ),
  192. ) );
  193. /**
  194. * Filter a report sales returned from the API.
  195. *
  196. * Allows modification of the report sales data right before it is returned.
  197. *
  198. * @param WP_REST_Response $response The response object.
  199. * @param stdClass $data The original report object.
  200. * @param WP_REST_Request $request Request used to generate the response.
  201. */
  202. return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request );
  203. }
  204. /**
  205. * Setup the report object and parse any date filtering.
  206. *
  207. * @param array $filter date filtering
  208. */
  209. protected function setup_report( $filter ) {
  210. include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' );
  211. include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' );
  212. $this->report = new WC_Report_Sales_By_Date();
  213. if ( empty( $filter['period'] ) ) {
  214. // Custom date range.
  215. $filter['period'] = 'custom';
  216. if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) {
  217. // Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges.
  218. $_GET['start_date'] = $filter['date_min'];
  219. $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null;
  220. } else {
  221. // Default custom range to today.
  222. $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) );
  223. }
  224. } else {
  225. $filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period'];
  226. // Change "week" period to "7day".
  227. if ( 'week' === $filter['period'] ) {
  228. $filter['period'] = '7day';
  229. }
  230. }
  231. $this->report->calculate_current_range( $filter['period'] );
  232. }
  233. /**
  234. * Get the Report's schema, conforming to JSON Schema.
  235. *
  236. * @return array
  237. */
  238. public function get_item_schema() {
  239. $schema = array(
  240. '$schema' => 'http://json-schema.org/draft-04/schema#',
  241. 'title' => 'sales_report',
  242. 'type' => 'object',
  243. 'properties' => array(
  244. 'total_sales' => array(
  245. 'description' => __( 'Gross sales in the period.', 'woocommerce' ),
  246. 'type' => 'string',
  247. 'context' => array( 'view' ),
  248. 'readonly' => true,
  249. ),
  250. 'net_sales' => array(
  251. 'description' => __( 'Net sales in the period.', 'woocommerce' ),
  252. 'type' => 'string',
  253. 'context' => array( 'view' ),
  254. 'readonly' => true,
  255. ),
  256. 'average_sales' => array(
  257. 'description' => __( 'Average net daily sales.', 'woocommerce' ),
  258. 'type' => 'string',
  259. 'context' => array( 'view' ),
  260. 'readonly' => true,
  261. ),
  262. 'total_orders' => array(
  263. 'description' => __( 'Total of orders placed.', 'woocommerce' ),
  264. 'type' => 'integer',
  265. 'context' => array( 'view' ),
  266. 'readonly' => true,
  267. ),
  268. 'total_items' => array(
  269. 'description' => __( 'Total of items purchased.', 'woocommerce' ),
  270. 'type' => 'integer',
  271. 'context' => array( 'view' ),
  272. 'readonly' => true,
  273. ),
  274. 'total_tax' => array(
  275. 'description' => __( 'Total charged for taxes.', 'woocommerce' ),
  276. 'type' => 'string',
  277. 'context' => array( 'view' ),
  278. 'readonly' => true,
  279. ),
  280. 'total_shipping' => array(
  281. 'description' => __( 'Total charged for shipping.', 'woocommerce' ),
  282. 'type' => 'string',
  283. 'context' => array( 'view' ),
  284. 'readonly' => true,
  285. ),
  286. 'total_refunds' => array(
  287. 'description' => __( 'Total of refunded orders.', 'woocommerce' ),
  288. 'type' => 'integer',
  289. 'context' => array( 'view' ),
  290. 'readonly' => true,
  291. ),
  292. 'total_discount' => array(
  293. 'description' => __( 'Total of coupons used.', 'woocommerce' ),
  294. 'type' => 'integer',
  295. 'context' => array( 'view' ),
  296. 'readonly' => true,
  297. ),
  298. 'totals_grouped_by' => array(
  299. 'description' => __( 'Group type.', 'woocommerce' ),
  300. 'type' => 'string',
  301. 'context' => array( 'view' ),
  302. 'readonly' => true,
  303. ),
  304. 'totals' => array(
  305. 'description' => __( 'Totals.', 'woocommerce' ),
  306. 'type' => 'array',
  307. 'items' => array(
  308. 'type' => 'array',
  309. ),
  310. 'context' => array( 'view' ),
  311. 'readonly' => true,
  312. ),
  313. ),
  314. );
  315. return $this->add_additional_fields_schema( $schema );
  316. }
  317. /**
  318. * Get the query params for collections.
  319. *
  320. * @return array
  321. */
  322. public function get_collection_params() {
  323. return array(
  324. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  325. 'period' => array(
  326. 'description' => __( 'Report period.', 'woocommerce' ),
  327. 'type' => 'string',
  328. 'enum' => array( 'week', 'month', 'last_month', 'year' ),
  329. 'validate_callback' => 'rest_validate_request_arg',
  330. 'sanitize_callback' => 'sanitize_text_field',
  331. ),
  332. 'date_min' => array(
  333. /* translators: %s: date format */
  334. 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ),
  335. 'type' => 'string',
  336. 'format' => 'date',
  337. 'validate_callback' => 'wc_rest_validate_reports_request_arg',
  338. 'sanitize_callback' => 'sanitize_text_field',
  339. ),
  340. 'date_max' => array(
  341. /* translators: %s: date format */
  342. 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ),
  343. 'type' => 'string',
  344. 'format' => 'date',
  345. 'validate_callback' => 'wc_rest_validate_reports_request_arg',
  346. 'sanitize_callback' => 'sanitize_text_field',
  347. ),
  348. );
  349. }
  350. }