diff --git a/plugins/woocommerce-admin/client/analytics/settings/config.js b/plugins/woocommerce-admin/client/analytics/settings/config.js index 0473ecd9404..467d559458c 100644 --- a/plugins/woocommerce-admin/client/analytics/settings/config.js +++ b/plugins/woocommerce-admin/client/analytics/settings/config.js @@ -4,7 +4,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { applyFilters } from '@wordpress/hooks'; import interpolateComponents from 'interpolate-components'; -import { ORDER_STATUSES } from '@woocommerce/wc-admin-settings'; +import { getSetting, ORDER_STATUSES } from '@woocommerce/wc-admin-settings'; /** * Internal dependencies @@ -25,8 +25,8 @@ export const DEFAULT_ORDER_STATUSES = [ export const DEFAULT_DATE_RANGE = 'period=month&compare=previous_year'; const filteredOrderStatuses = Object.keys( ORDER_STATUSES ) - .filter( status => status !== 'refunded' ) - .map( key => { + .filter( ( status ) => status !== 'refunded' ) + .map( ( key ) => { return { value: key, label: ORDER_STATUSES[ key ], @@ -37,25 +37,46 @@ const filteredOrderStatuses = Object.keys( ORDER_STATUSES ) }; } ); +const unregisteredOrderStatuses = getSetting( 'unregisteredOrderStatuses', {} ); + +const orderStatusOptions = [ + { + key: 'defaultStatuses', + options: filteredOrderStatuses.filter( ( status ) => + DEFAULT_ORDER_STATUSES.includes( status.value ) + ), + }, + { + key: 'customStatuses', + label: __( 'Custom Statuses', 'woocommerce-admin' ), + options: filteredOrderStatuses.filter( + ( status ) => ! DEFAULT_ORDER_STATUSES.includes( status.value ) + ), + }, + { + key: 'unregisteredStatuses', + label: __( 'Unregistered Statuses', 'woocommerce-admin' ), + options: Object.keys( unregisteredOrderStatuses ).map( ( key ) => { + return { + value: key, + label: key, + description: sprintf( + __( + 'Exclude the %s status from reports', + 'woocommerce-admin' + ), + key + ), + }; + } ), + }, +]; + export const config = applyFilters( SETTINGS_FILTER, { woocommerce_excluded_report_order_statuses: { label: __( 'Excluded Statuses:', 'woocommerce-admin' ), inputType: 'checkboxGroup', - options: [ - { - key: 'defaultStatuses', - options: filteredOrderStatuses.filter( status => - DEFAULT_ORDER_STATUSES.includes( status.value ) - ), - }, - { - key: 'customStatuses', - label: __( 'Custom Statuses', 'woocommerce-admin' ), - options: filteredOrderStatuses.filter( - status => ! DEFAULT_ORDER_STATUSES.includes( status.value ) - ), - }, - ], + options: orderStatusOptions, helpText: interpolateComponents( { mixedString: __( 'Orders with these statuses are excluded from the totals in your reports. ' + @@ -71,21 +92,7 @@ export const config = applyFilters( SETTINGS_FILTER, { woocommerce_actionable_order_statuses: { label: __( 'Actionable Statuses:', 'woocommerce-admin' ), inputType: 'checkboxGroup', - options: [ - { - key: 'defaultStatuses', - options: filteredOrderStatuses.filter( status => - DEFAULT_ORDER_STATUSES.includes( status.value ) - ), - }, - { - key: 'customStatuses', - label: __( 'Custom Statuses', 'woocommerce-admin' ), - options: filteredOrderStatuses.filter( - status => ! DEFAULT_ORDER_STATUSES.includes( status.value ) - ), - }, - ], + options: orderStatusOptions, helpText: __( 'Orders with these statuses require action on behalf of the store admin.' + 'These orders will show up in the Orders tab under the activity panel.', diff --git a/plugins/woocommerce-admin/src/API/Reports/Orders/DataStore.php b/plugins/woocommerce-admin/src/API/Reports/Orders/DataStore.php index 59da168f065..735af901633 100644 --- a/plugins/woocommerce-admin/src/API/Reports/Orders/DataStore.php +++ b/plugins/woocommerce-admin/src/API/Reports/Orders/DataStore.php @@ -12,6 +12,7 @@ defined( 'ABSPATH' ) || exit; use \Automattic\WooCommerce\Admin\API\Reports\DataStore as ReportsDataStore; use \Automattic\WooCommerce\Admin\API\Reports\DataStoreInterface; use \Automattic\WooCommerce\Admin\API\Reports\SqlQuery; +use \Automattic\WooCommerce\Admin\API\Reports\Cache; /** * API\Reports\Orders\DataStore. @@ -432,6 +433,29 @@ class DataStore extends ReportsDataStore implements DataStoreInterface { return $coupons; } + /** + * Get all statuses that have been synced. + * + * @return array Unique order statuses. + */ + public static function get_all_statuses() { + global $wpdb; + + $cache_key = 'orders-all-statuses'; + $statuses = Cache::get( $cache_key ); + + if ( false === $statuses ) { + $table_name = self::get_db_table_name(); + $statuses = $wpdb->get_col( + "SELECT DISTINCT status FROM {$table_name}" + ); // WPCS: cache ok, DB call ok, unprepared SQL ok. + + Cache::set( $cache_key, $statuses ); + } + + return $statuses; + } + /** * Initialize query objects. */ diff --git a/plugins/woocommerce-admin/src/Loader.php b/plugins/woocommerce-admin/src/Loader.php index f52f3d40ea0..a41b94491fe 100644 --- a/plugins/woocommerce-admin/src/Loader.php +++ b/plugins/woocommerce-admin/src/Loader.php @@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\Admin; use \_WP_Dependency; use Automattic\WooCommerce\Admin\Features\Onboarding; +use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore; /** * Loader Class. @@ -73,8 +74,6 @@ class Loader { add_action( 'in_admin_header', array( __CLASS__, 'embed_page_header' ) ); add_filter( 'woocommerce_settings_groups', array( __CLASS__, 'add_settings_group' ) ); add_filter( 'woocommerce_settings-wc_admin', array( __CLASS__, 'add_settings' ) ); - add_filter( 'option_woocommerce_actionable_order_statuses', array( __CLASS__, 'filter_invalid_statuses' ) ); - add_filter( 'option_woocommerce_excluded_report_order_statuses', array( __CLASS__, 'filter_invalid_statuses' ) ); add_action( 'admin_head', array( __CLASS__, 'remove_notices' ) ); add_action( 'admin_notices', array( __CLASS__, 'inject_before_notices' ), -9999 ); add_action( 'admin_notices', array( __CLASS__, 'inject_after_notices' ), PHP_INT_MAX ); @@ -724,6 +723,9 @@ class Loader { // WooCommerce Branding is an example of this - so pass through the translation of // 'WooCommerce' to wcSettings. $settings['woocommerceTranslation'] = __( 'WooCommerce', 'woocommerce-admin' ); + // We may have synced orders with a now-unregistered status. + // E.g An extension that added statuses is now inactive or removed. + $settings['unregisteredOrderStatuses'] = self::get_unregistered_order_statuses(); if ( ! empty( $preload_data_endpoints ) ) { $settings['dataEndpoints'] = isset( $settings['dataEndpoints'] ) @@ -760,6 +762,21 @@ class Loader { return $formatted_statuses; } + /** + * Get all order statuses present in analytics tables that aren't registered. + * + * @return array Unregistered order statuses. + */ + public static function get_unregistered_order_statuses() { + $registered_statuses = wc_get_order_statuses(); + $all_synced_statuses = OrdersDataStore::get_all_statuses(); + $unregistered_statuses = array_diff( $all_synced_statuses, array_keys( $registered_statuses ) ); + $formatted_status_keys = self::get_order_statuses( array_fill_keys( $unregistered_statuses, '' ) ); + $formatted_statuses = array_keys( $formatted_status_keys ); + + return array_combine( $formatted_statuses, $formatted_statuses ); + } + /** * Register the admin settings for use in the WC REST API * @@ -782,7 +799,10 @@ class Loader { * @return array */ public static function add_settings( $settings ) { - $statuses = self::get_order_statuses( wc_get_order_statuses() ); + $unregistered_statuses = self::get_unregistered_order_statuses(); + $registered_statuses = self::get_order_statuses( wc_get_order_statuses() ); + $all_statuses = array_merge( $unregistered_statuses, $registered_statuses ); + $settings[] = array( 'id' => 'woocommerce_excluded_report_order_statuses', 'option_key' => 'woocommerce_excluded_report_order_statuses', @@ -790,7 +810,7 @@ class Loader { 'description' => __( 'Statuses that should not be included when calculating report totals.', 'woocommerce-admin' ), 'default' => array( 'pending', 'cancelled', 'failed' ), 'type' => 'multiselect', - 'options' => $statuses, + 'options' => $all_statuses, ); $settings[] = array( 'id' => 'woocommerce_actionable_order_statuses', @@ -799,7 +819,7 @@ class Loader { 'description' => __( 'Statuses that require extra action on behalf of the store admin.', 'woocommerce-admin' ), 'default' => array( 'processing', 'on-hold' ), 'type' => 'multiselect', - 'options' => $statuses, + 'options' => $all_statuses, ); $settings[] = array( 'id' => 'woocommerce_default_date_range', @@ -812,21 +832,6 @@ class Loader { return $settings; } - /** - * Filter invalid statuses from saved settings to avoid removed statuses throwing errors. - * - * @param array|null $value Saved order statuses. - * @return array|null - */ - public static function filter_invalid_statuses( $value ) { - if ( is_array( $value ) ) { - $valid_statuses = array_keys( self::get_order_statuses( wc_get_order_statuses() ) ); - $value = array_intersect( $value, $valid_statuses ); - } - - return $value; - } - /** * Gets custom settings used for WC Admin. *