WooCommerce Code Reference

ActivityPanelCounts.php

Source code

<?php
/**
 * REST API ActivityPanelCounts Controller
 *
 * Handles requests to /activity-panel/counts.
 */

declare( strict_types=1 );

namespace Automattic\WooCommerce\Admin\API;

defined( 'ABSPATH' ) || exit;

/**
 * ActivityPanelCounts controller.
 *
 * @internal
 */
class ActivityPanelCounts extends \WC_REST_Data_Controller {

	/**
	 * Endpoint namespace.
	 *
	 * @var string
	 */
	protected $namespace = 'wc-analytics';

	/**
	 * Route base.
	 *
	 * @var string
	 */
	protected $rest_base = 'activity-panel/counts';

	/**
	 * Register routes.
	 */
	public function register_routes(): void {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_counts' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_counts_params(),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);
	}

	/**
	 * Return the orders/reviews/low-stock counts used by the Activity Panel in one response,
	 * instead of one request per count.
	 *
	 * @param \WP_REST_Request<array<string, mixed>> $request Request object.
	 * @return \WP_REST_Response
	 */
	public function get_counts( $request ) {
		$order_statuses = (array) $request->get_param( 'order_statuses' );

		// When a merchant has cleared every actionable order status there is nothing
		// "to fulfill". Short-circuit to 0 rather than querying: an empty status list
		// would otherwise count every order, and the previous client-side
		// getUnreadOrders() returned 0 in this case.
		$orders_to_fulfill_count = empty( $order_statuses )
			? 0
			: $this->get_count_via(
				'/wc-analytics/orders',
				array(
					'page'     => 1,
					'per_page' => 1,
					'status'   => $order_statuses,
					'_fields'  => array( 'id' ),
				)
			);

		return rest_ensure_response(
			array(
				'orders_to_fulfill_count'     => $orders_to_fulfill_count,
				'reviews_to_moderate_count'   => $this->get_count_via(
					'/wc-analytics/products/reviews',
					array(
						'page'     => 1,
						'per_page' => 1,
						'status'   => $request->get_param( 'review_status' ),
						'_fields'  => array( 'id' ),
					)
				),
				'products_low_in_stock_count' => $this->get_count_via(
					'/wc-analytics/products/count-low-in-stock',
					array( 'status' => $request->get_param( 'product_status' ) )
				),
			)
		);
	}

	/**
	 * Run one of the existing count endpoints internally and read its total off the response,
	 * so the counting logic itself (query building, permission checks) isn't duplicated here.
	 *
	 * @param string $route  REST route to call, e.g. '/wc-analytics/orders'.
	 * @param array  $params Query params for the sub-request.
	 * @return int|null Null when the sub-request failed, so callers can tell "unknown" apart from a real zero count.
	 */
	private function get_count_via( $route, $params ) {
		$sub_request = new \WP_REST_Request( 'GET', $route );
		foreach ( $params as $key => $value ) {
			$sub_request->set_param( $key, $value );
		}

		$response = rest_do_request( $sub_request );

		if ( $response->is_error() ) {
			wc_get_logger()->warning(
				sprintf( 'Activity Panel counts sub-request to %s failed.', $route ),
				array( 'source' => 'activity-panel-counts' )
			);
			return null;
		}

		$headers = $response->get_headers();
		if ( isset( $headers['X-WP-Total'] ) ) {
			return (int) $headers['X-WP-Total'];
		}

		$data = $response->get_data();
		return isset( $data['total'] ) ? (int) $data['total'] : null;
	}

	/**
	 * Get the query params for the /activity-panel/counts endpoint.
	 *
	 * @return array
	 */
	public function get_counts_params() {
		$params                   = array();
		$params['context']        = $this->get_context_param( array( 'default' => 'view' ) );
		$params['order_statuses'] = array(
			'description'       => __( 'Order statuses counted as "to fulfill".', 'woocommerce' ),
			'type'              => 'array',
			'items'             => array( 'type' => 'string' ),
			'default'           => $this->get_default_order_statuses(),
			'sanitize_callback' => 'wp_parse_list',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['review_status']  = array(
			'description'       => __( 'Review status counted as "to moderate".', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'hold',
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);
		$params['product_status'] = array(
			'description'       => __( 'Product post status used for the low stock count.', 'woocommerce' ),
			'type'              => 'string',
			'default'           => 'publish',
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);

		return $params;
	}

	/**
	 * Get the default order statuses counted as "to fulfill", matching the store's own
	 * actionable order statuses setting.
	 *
	 * @return array
	 */
	private function get_default_order_statuses() {
		$actionable = get_option( 'woocommerce_actionable_order_statuses', false );

		// Any array is respected as-is, including an explicitly empty one: the merchant
		// intentionally cleared all actionable statuses, so there is nothing to fulfill,
		// matching the previous client-side behaviour. A missing (never configured) or
		// malformed option falls back to the built-in defaults.
		return is_array( $actionable ) ? $actionable : array( 'processing', 'on-hold' );
	}

	/**
	 * Get the schema for the /activity-panel/counts response.
	 *
	 * @return array
	 */
	public function get_item_schema() {
		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'activity_panel_counts',
			'type'       => 'object',
			'properties' => array(
				'orders_to_fulfill_count'     => array(
					'description' => __( 'Number of orders to fulfill. Null if the underlying sub-request failed.', 'woocommerce' ),
					'type'        => array( 'integer', 'null' ),
				),
				'reviews_to_moderate_count'   => array(
					'description' => __( 'Number of reviews awaiting moderation. Null if the underlying sub-request failed.', 'woocommerce' ),
					'type'        => array( 'integer', 'null' ),
				),
				'products_low_in_stock_count' => array(
					'description' => __( 'Number of products low in stock. Null if the underlying sub-request failed.', 'woocommerce' ),
					'type'        => array( 'integer', 'null' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $schema );
	}
}