Commit 96d526cd2b2 for woocommerce
commit 96d526cd2b2289321edfac80821d2027be455d3e
Author: Miroslav Mitev <m1r0@users.noreply.github.com>
Date: Fri Oct 16 13:01:45 2026 +0300
Add a payment method filter to the Analytics Orders report (#69651)
* Add a payment method filter to the Analytics Orders report
* Read the payment method filter options through a cached Orders data store method
* Add a payment method and date paid index to the order stats table
EXPLAIN on 1M orders showed the filter queries and the filter options DISTINCT query doing full table scans. A payment_method-only key made a dominant gateway over a year slower, and a (date, payment_method) key didn't change the plan. (payment_method, date_paid) covers the default date type and the options query.
* Add the status to the order stats payment method index
With the status in the index, the Orders report count query reads only the index. On 1M orders that takes a dominant gateway over a year from a 259ms full scan to 37ms. Other report queries stay the same, and the index grows from 31.6MB to 46.6MB.
diff --git a/plugins/woocommerce/changelog/47283-analytics-orders-payment-method-filter b/plugins/woocommerce/changelog/47283-analytics-orders-payment-method-filter
new file mode 100644
index 00000000000..d32290e6614
--- /dev/null
+++ b/plugins/woocommerce/changelog/47283-analytics-orders-payment-method-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Analytics: add a payment method advanced filter to the Orders report.
diff --git a/plugins/woocommerce/client/admin/client/analytics/report/orders/config.js b/plugins/woocommerce/client/admin/client/analytics/report/orders/config.js
index 139a14a5b6b..ec0927a17cb 100644
--- a/plugins/woocommerce/client/admin/client/analytics/report/orders/config.js
+++ b/plugins/woocommerce/client/admin/client/analytics/report/orders/config.js
@@ -13,7 +13,7 @@ import {
getTaxRateLabels,
getVariationLabels,
} from '../../../lib/async-requests';
-import { ORDER_STATUSES } from '~/utils/admin-settings';
+import { ORDER_STATUSES, PAYMENT_GATEWAYS } from '~/utils/admin-settings';
const ORDERS_REPORT_CHARTS_FILTER = 'woocommerce_admin_orders_report_charts';
const ORDERS_REPORT_FILTERS_FILTER = 'woocommerce_admin_orders_report_filters';
@@ -264,6 +264,40 @@ export const advancedFilters = applyFilters(
defaultOption: 'new',
},
},
+ payment_method: {
+ labels: {
+ add: __( 'Payment method', 'woocommerce' ),
+ remove: __( 'Remove payment method filter', 'woocommerce' ),
+ rule: __(
+ 'Select a payment method filter match',
+ 'woocommerce'
+ ),
+ /* translators: A sentence describing a Payment method filter. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ */
+ title: __(
+ '<title>Payment method</title> <rule/> <filter/>',
+ 'woocommerce'
+ ),
+ filter: __( 'Select a payment method', 'woocommerce' ),
+ },
+ rules: [
+ {
+ value: 'is',
+ /* translators: Sentence fragment, logical, "Is" refers to searching for orders paid with a chosen payment method. Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
+ label: _x( 'Is', 'payment method', 'woocommerce' ),
+ },
+ {
+ value: 'is_not',
+ /* translators: Sentence fragment, logical, "Is Not" refers to searching for orders not paid with a chosen payment method. Screenshot for context: https://cloudup.com/cSsUY9VeCVJ */
+ label: _x( 'Is Not', 'payment method', 'woocommerce' ),
+ },
+ ],
+ input: {
+ component: 'SelectControl',
+ options: Object.entries( PAYMENT_GATEWAYS ).map(
+ ( [ value, label ] ) => ( { value, label } )
+ ),
+ },
+ },
refunds: {
labels: {
add: __( 'Refund', 'woocommerce' ),
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 17ccf82509c..b6aa1f6bfbc 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -1499,7 +1499,8 @@ class WC_Install {
KEY status (status)' .
( $should_have_fulfillment_column ? ',
KEY fulfillment_status (fulfillment_status)' : '' ) . ",
- KEY idx_date_paid_status_parent (date_paid, status, parent_id)
+ KEY idx_date_paid_status_parent (date_paid, status, parent_id),
+ KEY idx_payment_method_date_paid_status (payment_method, date_paid, status)
) $collate;";
}
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index fbea245c1f0..4ecf878915a 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -20,6 +20,7 @@ defined( 'ABSPATH' ) || exit;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
@@ -4213,6 +4214,8 @@ function wc_update_1130_backfill_order_stats_payment_method() {
// The report rows cached before the column was filled would otherwise be served for a week.
ReportsCache::invalidate();
+ // The same goes for the Orders report payment method filter, cached for a year.
+ wp_cache_delete( OrdersDataStore::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, 'woocommerce_analytics' );
return false;
}
diff --git a/plugins/woocommerce/src/Admin/API/Reports/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/DataStore.php
index 2b7aa8d2eab..7f163dde402 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/DataStore.php
@@ -1488,6 +1488,60 @@ class DataStore extends SqlQuery implements DataStoreInterface {
}
}
+ /**
+ * Returns payment gateway subquery to be used in WHERE SQL query, based on query arguments from the user.
+ *
+ * The include and exclude clauses are always joined with AND, so that "is bacs" and "is not cheque"
+ * narrow the result together even under `match=any`.
+ *
+ * @since 11.3.0
+ *
+ * @param array $query_args Parameters supplied by the user.
+ * @return string
+ */
+ protected function get_payment_method_subquery( $query_args ) {
+ global $wpdb;
+
+ $table = $wpdb->prefix . 'wc_order_stats';
+ $subqueries = array();
+
+ $included = $this->get_payment_method_list( $query_args, 'payment_method_is' );
+ if ( $included ) {
+ $subqueries[] = "{$table}.payment_method IN ( {$included} )";
+ }
+
+ $excluded = $this->get_payment_method_list( $query_args, 'payment_method_is_not' );
+ if ( $excluded ) {
+ // NOT IN would drop the NULL rows, which are not the excluded gateway either.
+ $subqueries[] = "( {$table}.payment_method IS NULL OR {$table}.payment_method NOT IN ( {$excluded} ) )";
+ }
+
+ return implode( ' AND ', $subqueries );
+ }
+
+ /**
+ * Build a quoted SQL list from the payment gateway ids held in a query argument.
+ *
+ * @param array $query_args Parameters supplied by the user.
+ * @param string $field Query argument holding the ids.
+ * @return string Comma separated list of quoted ids, empty when there is nothing to filter on.
+ */
+ private function get_payment_method_list( $query_args, $field ) {
+ $payment_methods = isset( $query_args[ $field ] ) && is_array( $query_args[ $field ] ) ? $query_args[ $field ] : array();
+ $payment_methods = array_filter(
+ array_map( 'strval', $payment_methods ),
+ function ( $payment_method ) {
+ return '' !== $payment_method;
+ }
+ );
+
+ if ( ! $payment_methods ) {
+ return '';
+ }
+
+ return "'" . implode( "','", array_map( 'esc_sql', $payment_methods ) ) . "'";
+ }
+
/**
* Returns customer subquery to be used in WHERE SQL query, based on query arguments from the user.
*
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php
index 835ebc1ca5b..636f5b80ee2 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Controller.php
@@ -50,32 +50,34 @@ class Controller extends GenericController implements ExportableInterface {
* @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['orderby'] = $request['orderby'];
- $args['order'] = $request['order'];
- $args['product_includes'] = (array) $request['product_includes'];
- $args['product_excludes'] = (array) $request['product_excludes'];
- $args['variation_includes'] = (array) $request['variation_includes'];
- $args['variation_excludes'] = (array) $request['variation_excludes'];
- $args['coupon_includes'] = (array) $request['coupon_includes'];
- $args['coupon_excludes'] = (array) $request['coupon_excludes'];
- $args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
- $args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
- $args['status_is'] = (array) $request['status_is'];
- $args['status_is_not'] = (array) $request['status_is_not'];
- $args['customer_type'] = $request['customer_type'];
- $args['extended_info'] = $request['extended_info'];
- $args['refunds'] = $request['refunds'];
- $args['match'] = $request['match'];
- $args['order_includes'] = $request['order_includes'];
- $args['order_excludes'] = $request['order_excludes'];
- $args['attribute_is'] = (array) $request['attribute_is'];
- $args['attribute_is_not'] = (array) $request['attribute_is_not'];
- $args['force_cache_refresh'] = $request['force_cache_refresh'];
+ $args = array();
+ $args['before'] = $request['before'];
+ $args['after'] = $request['after'];
+ $args['page'] = $request['page'];
+ $args['per_page'] = $request['per_page'];
+ $args['orderby'] = $request['orderby'];
+ $args['order'] = $request['order'];
+ $args['product_includes'] = (array) $request['product_includes'];
+ $args['product_excludes'] = (array) $request['product_excludes'];
+ $args['variation_includes'] = (array) $request['variation_includes'];
+ $args['variation_excludes'] = (array) $request['variation_excludes'];
+ $args['coupon_includes'] = (array) $request['coupon_includes'];
+ $args['coupon_excludes'] = (array) $request['coupon_excludes'];
+ $args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
+ $args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
+ $args['status_is'] = (array) $request['status_is'];
+ $args['status_is_not'] = (array) $request['status_is_not'];
+ $args['customer_type'] = $request['customer_type'];
+ $args['payment_method_is'] = (array) $request['payment_method_is'];
+ $args['payment_method_is_not'] = (array) $request['payment_method_is_not'];
+ $args['extended_info'] = $request['extended_info'];
+ $args['refunds'] = $request['refunds'];
+ $args['match'] = $request['match'];
+ $args['order_includes'] = $request['order_includes'];
+ $args['order_excludes'] = $request['order_excludes'];
+ $args['attribute_is'] = (array) $request['attribute_is'];
+ $args['attribute_is_not'] = (array) $request['attribute_is_not'];
+ $args['force_cache_refresh'] = $request['force_cache_refresh'];
return $args;
}
@@ -237,16 +239,16 @@ class Controller extends GenericController implements ExportableInterface {
* @return array
*/
public function get_collection_params() {
- $params = parent::get_collection_params();
- $params['per_page']['minimum'] = 0;
- $params['orderby']['enum'] = $this->apply_custom_orderby_filters(
+ $params = parent::get_collection_params();
+ $params['per_page']['minimum'] = 0;
+ $params['orderby']['enum'] = $this->apply_custom_orderby_filters(
array(
'date',
'num_items_sold',
'net_total',
)
);
- $params['product_includes'] = array(
+ $params['product_includes'] = array(
'description' => __( 'Limit result set to items that have the specified product(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -256,7 +258,7 @@ class Controller extends GenericController implements ExportableInterface {
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['product_excludes'] = array(
+ $params['product_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified product(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -266,7 +268,7 @@ class Controller extends GenericController implements ExportableInterface {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['variation_includes'] = array(
+ $params['variation_includes'] = array(
'description' => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -276,7 +278,7 @@ class Controller extends GenericController implements ExportableInterface {
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['variation_excludes'] = array(
+ $params['variation_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -286,7 +288,7 @@ class Controller extends GenericController implements ExportableInterface {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['coupon_includes'] = array(
+ $params['coupon_includes'] = array(
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -296,7 +298,7 @@ class Controller extends GenericController implements ExportableInterface {
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['coupon_excludes'] = array(
+ $params['coupon_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -306,7 +308,7 @@ class Controller extends GenericController implements ExportableInterface {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['tax_rate_includes'] = array(
+ $params['tax_rate_includes'] = array(
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -316,7 +318,7 @@ class Controller extends GenericController implements ExportableInterface {
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['tax_rate_excludes'] = array(
+ $params['tax_rate_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -326,7 +328,7 @@ class Controller extends GenericController implements ExportableInterface {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['status_is'] = array(
+ $params['status_is'] = array(
'description' => __( 'Limit result set to items that have the specified order status.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
@@ -336,7 +338,7 @@ class Controller extends GenericController implements ExportableInterface {
'type' => 'string',
),
);
- $params['status_is_not'] = array(
+ $params['status_is_not'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order status.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_slug_list',
@@ -346,7 +348,7 @@ class Controller extends GenericController implements ExportableInterface {
'type' => 'string',
),
);
- $params['customer_type'] = array(
+ $params['customer_type'] = array(
'description' => __( 'Limit result set to returning or new customers.', 'woocommerce' ),
'type' => 'string',
'default' => '',
@@ -357,7 +359,27 @@ class Controller extends GenericController implements ExportableInterface {
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['refunds'] = array(
+ $params['payment_method_is'] = array(
+ 'description' => __( 'Limit result set to items that were paid with the specified payment gateway(s).', 'woocommerce' ),
+ 'type' => 'array',
+ 'default' => array(),
+ 'sanitize_callback' => 'wp_parse_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ );
+ $params['payment_method_is_not'] = array(
+ 'description' => __( 'Limit result set to items that were not paid with the specified payment gateway(s).', 'woocommerce' ),
+ 'type' => 'array',
+ 'default' => array(),
+ 'sanitize_callback' => 'wp_parse_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ );
+ $params['refunds'] = array(
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce' ),
'type' => 'string',
'default' => '',
@@ -370,14 +392,14 @@ class Controller extends GenericController implements ExportableInterface {
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['extended_info'] = array(
+ $params['extended_info'] = array(
'description' => __( 'Add additional piece of info about each coupon to the report.', 'woocommerce' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => 'wc_string_to_bool',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['order_includes'] = array(
+ $params['order_includes'] = array(
'description' => __( 'Limit result set to items that have the specified order ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@@ -386,7 +408,7 @@ class Controller extends GenericController implements ExportableInterface {
'type' => 'integer',
),
);
- $params['order_excludes'] = array(
+ $params['order_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified order ids.', 'woocommerce' ),
'type' => 'array',
'sanitize_callback' => 'wp_parse_id_list',
@@ -395,7 +417,7 @@ class Controller extends GenericController implements ExportableInterface {
'type' => 'integer',
),
);
- $params['attribute_is'] = array(
+ $params['attribute_is'] = array(
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -404,7 +426,7 @@ class Controller extends GenericController implements ExportableInterface {
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['attribute_is_not'] = array(
+ $params['attribute_is_not'] = array(
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
index 94dfa835e48..6f4b79bb4cc 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
@@ -26,6 +26,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
const ORDERS_STATUSES_ALL_CACHE_KEY = 'woocommerce_analytics_orders_statuses_all';
+ /**
+ * The cache key for payment methods.
+ */
+ const ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY = 'woocommerce_analytics_orders_payment_methods_all';
+
/**
* Dynamically sets the date column name based on configuration
*
@@ -43,6 +48,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
*/
final public static function init() {
add_action( 'woocommerce_analytics_update_order_stats', array( __CLASS__, 'maybe_update_order_statuses_cache' ) );
+ add_action( 'woocommerce_analytics_update_order_stats', array( __CLASS__, 'maybe_update_payment_methods_cache' ) );
}
/**
@@ -172,6 +178,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$where_subquery[] = "{$order_stats_lookup_table}.returning_customer = {$returning_customer}";
}
+ $payment_method_subquery = $this->get_payment_method_subquery( $query_args );
+ if ( $payment_method_subquery ) {
+ $where_subquery[] = $payment_method_subquery;
+ }
+
$refund_subquery = $this->get_refund_subquery( $query_args );
$this->subquery->add_sql_clause( 'from', $refund_subquery['from_clause'] );
if ( $refund_subquery['where_clause'] ) {
@@ -261,19 +272,21 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$defaults = array_merge(
parent::get_default_query_vars(),
array(
- 'orderby' => $this->date_column_name,
- 'product_includes' => array(),
- 'product_excludes' => array(),
- 'coupon_includes' => array(),
- 'coupon_excludes' => array(),
- 'tax_rate_includes' => array(),
- 'tax_rate_excludes' => array(),
- 'customer_type' => null,
- 'status_is' => array(),
- 'extended_info' => false,
- 'refunds' => null,
- 'order_includes' => array(),
- 'order_excludes' => array(),
+ 'orderby' => $this->date_column_name,
+ 'product_includes' => array(),
+ 'product_excludes' => array(),
+ 'coupon_includes' => array(),
+ 'coupon_excludes' => array(),
+ 'tax_rate_includes' => array(),
+ 'tax_rate_excludes' => array(),
+ 'customer_type' => null,
+ 'payment_method_is' => array(),
+ 'payment_method_is_not' => array(),
+ 'status_is' => array(),
+ 'extended_info' => false,
+ 'refunds' => null,
+ 'order_includes' => array(),
+ 'order_excludes' => array(),
)
);
@@ -767,6 +780,51 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
self::maybe_update_order_statuses_cache( $order_id );
}
+ /**
+ * Get all payment methods that have been synced.
+ *
+ * @since 11.3.0
+ *
+ * @return string[] Unique payment method ids.
+ */
+ public static function get_all_payment_methods() {
+ global $wpdb;
+
+ $payment_methods = wp_cache_get( self::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, 'woocommerce_analytics' );
+ if ( false === $payment_methods ) {
+ $table_name = self::get_db_table_name();
+ $payment_methods = $wpdb->get_col(
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name cannot be prepared.
+ "SELECT DISTINCT payment_method FROM {$table_name} WHERE payment_method != '' ORDER BY payment_method"
+ );
+ wp_cache_set( self::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, $payment_methods, 'woocommerce_analytics', YEAR_IN_SECONDS );
+ }
+
+ return $payment_methods;
+ }
+
+ /**
+ * Ensure the order payment method is present in the `get_all_payment_methods` call result.
+ *
+ * A refund has no payment method of its own. The order it refunds adds it.
+ *
+ * @internal
+ *
+ * @param int $order_id Order ID.
+ * @return void
+ */
+ public static function maybe_update_payment_methods_cache( $order_id ) {
+ $order = wc_get_order( $order_id );
+ $payment_method = $order instanceof \WC_Order ? $order->get_payment_method() : '';
+ if ( $payment_method ) {
+ $payment_methods = self::get_all_payment_methods();
+ if ( ! in_array( $payment_method, $payment_methods, true ) ) {
+ $payment_methods[] = $payment_method;
+ wp_cache_set( self::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, $payment_methods, 'woocommerce_analytics', YEAR_IN_SECONDS );
+ }
+ }
+ }
+
/**
* Initialize query objects.
*/
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/Controller.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/Controller.php
index f050291a0ce..db1c0432bc1 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/Controller.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/Controller.php
@@ -50,33 +50,35 @@ class Controller extends GenericStatsController {
* @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['fields'] = $request['fields'];
- $args['match'] = $request['match'];
- $args['status_is'] = (array) $request['status_is'];
- $args['status_is_not'] = (array) $request['status_is_not'];
- $args['product_includes'] = (array) $request['product_includes'];
- $args['product_excludes'] = (array) $request['product_excludes'];
- $args['variation_includes'] = (array) $request['variation_includes'];
- $args['variation_excludes'] = (array) $request['variation_excludes'];
- $args['coupon_includes'] = (array) $request['coupon_includes'];
- $args['coupon_excludes'] = (array) $request['coupon_excludes'];
- $args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
- $args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
- $args['customer_type'] = $request['customer_type'];
- $args['refunds'] = $request['refunds'];
- $args['attribute_is'] = (array) $request['attribute_is'];
- $args['attribute_is_not'] = (array) $request['attribute_is_not'];
- $args['category_includes'] = (array) $request['categories'];
- $args['segmentby'] = $request['segmentby'];
- $args['force_cache_refresh'] = $request['force_cache_refresh'];
+ $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['fields'] = $request['fields'];
+ $args['match'] = $request['match'];
+ $args['status_is'] = (array) $request['status_is'];
+ $args['status_is_not'] = (array) $request['status_is_not'];
+ $args['product_includes'] = (array) $request['product_includes'];
+ $args['product_excludes'] = (array) $request['product_excludes'];
+ $args['variation_includes'] = (array) $request['variation_includes'];
+ $args['variation_excludes'] = (array) $request['variation_excludes'];
+ $args['coupon_includes'] = (array) $request['coupon_includes'];
+ $args['coupon_excludes'] = (array) $request['coupon_excludes'];
+ $args['tax_rate_includes'] = (array) $request['tax_rate_includes'];
+ $args['tax_rate_excludes'] = (array) $request['tax_rate_excludes'];
+ $args['customer_type'] = $request['customer_type'];
+ $args['payment_method_is'] = (array) $request['payment_method_is'];
+ $args['payment_method_is_not'] = (array) $request['payment_method_is_not'];
+ $args['refunds'] = $request['refunds'];
+ $args['attribute_is'] = (array) $request['attribute_is'];
+ $args['attribute_is_not'] = (array) $request['attribute_is_not'];
+ $args['category_includes'] = (array) $request['categories'];
+ $args['segmentby'] = $request['segmentby'];
+ $args['force_cache_refresh'] = $request['force_cache_refresh'];
// For backwards compatibility, `customer` is aliased to `customer_type`.
if ( empty( $request['customer_type'] ) && ! empty( $request['customer'] ) ) {
@@ -261,7 +263,7 @@ class Controller extends GenericStatsController {
'sanitize_callback' => 'wp_parse_id_list',
);
// Split assignments for PHPCS complaining on aligned.
- $params['variation_includes'] = array(
+ $params['variation_includes'] = array(
'description' => __( 'Limit result set to items that have the specified variation(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -271,7 +273,7 @@ class Controller extends GenericStatsController {
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['variation_excludes'] = array(
+ $params['variation_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified variation(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -281,7 +283,7 @@ class Controller extends GenericStatsController {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['coupon_includes'] = array(
+ $params['coupon_includes'] = array(
'description' => __( 'Limit result set to items that have the specified coupon(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -290,7 +292,7 @@ class Controller extends GenericStatsController {
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['coupon_excludes'] = array(
+ $params['coupon_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified coupon(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -299,7 +301,7 @@ class Controller extends GenericStatsController {
'default' => array(),
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['tax_rate_includes'] = array(
+ $params['tax_rate_includes'] = array(
'description' => __( 'Limit result set to items that have the specified tax rate(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -309,7 +311,7 @@ class Controller extends GenericStatsController {
'sanitize_callback' => 'wp_parse_id_list',
'validate_callback' => 'rest_validate_request_arg',
);
- $params['tax_rate_excludes'] = array(
+ $params['tax_rate_excludes'] = array(
'description' => __( 'Limit result set to items that don\'t have the specified tax rate(s) assigned.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -319,7 +321,7 @@ class Controller extends GenericStatsController {
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'wp_parse_id_list',
);
- $params['customer'] = array(
+ $params['customer'] = array(
'description' => __( 'Alias for customer_type (deprecated).', 'woocommerce' ),
'type' => 'string',
'enum' => array(
@@ -328,7 +330,27 @@ class Controller extends GenericStatsController {
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['customer_type'] = array(
+ $params['payment_method_is'] = array(
+ 'description' => __( 'Limit result set to items that were paid with the specified payment gateway(s).', 'woocommerce' ),
+ 'type' => 'array',
+ 'default' => array(),
+ 'sanitize_callback' => 'wp_parse_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ );
+ $params['payment_method_is_not'] = array(
+ 'description' => __( 'Limit result set to items that were not paid with the specified payment gateway(s).', 'woocommerce' ),
+ 'type' => 'array',
+ 'default' => array(),
+ 'sanitize_callback' => 'wp_parse_list',
+ 'validate_callback' => 'rest_validate_request_arg',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ );
+ $params['customer_type'] = array(
'description' => __( 'Limit result set to orders that have the specified customer_type', 'woocommerce' ),
'type' => 'string',
'enum' => array(
@@ -337,7 +359,7 @@ class Controller extends GenericStatsController {
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['refunds'] = array(
+ $params['refunds'] = array(
'description' => __( 'Limit result set to specific types of refunds.', 'woocommerce' ),
'type' => 'string',
'default' => '',
@@ -350,7 +372,7 @@ class Controller extends GenericStatsController {
),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['attribute_is'] = array(
+ $params['attribute_is'] = array(
'description' => __( 'Limit result set to orders that include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -359,7 +381,7 @@ class Controller extends GenericStatsController {
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['attribute_is_not'] = array(
+ $params['attribute_is_not'] = array(
'description' => __( 'Limit result set to orders that don\'t include products with the specified attributes.', 'woocommerce' ),
'type' => 'array',
'items' => array(
@@ -368,7 +390,7 @@ class Controller extends GenericStatsController {
'default' => array(),
'validate_callback' => 'rest_validate_request_arg',
);
- $params['segmentby'] = array(
+ $params['segmentby'] = array(
'description' => __( 'Segment the response by additional constraint.', 'woocommerce' ),
'type' => 'string',
'enum' => array(
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
index 03f9e9838ba..22be771b8b2 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -264,6 +264,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
}
$where_filters[] = $this->get_customer_subquery( $query_args );
+ $where_filters[] = $this->get_payment_method_subquery( $query_args );
$refund_subquery = $this->get_refund_subquery( $query_args );
$from_clause .= $refund_subquery['from_clause'];
if ( $refund_subquery['where_clause'] ) {
@@ -303,20 +304,22 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$defaults = array_merge(
parent::get_default_query_vars(),
array(
- 'interval' => 'week',
- 'segmentby' => '',
-
- 'match' => 'all',
- 'status_is' => array(),
- 'status_is_not' => array(),
- 'product_includes' => array(),
- 'product_excludes' => array(),
- 'coupon_includes' => array(),
- 'coupon_excludes' => array(),
- 'tax_rate_includes' => array(),
- 'tax_rate_excludes' => array(),
- 'customer_type' => '',
- 'category_includes' => array(),
+ 'interval' => 'week',
+ 'segmentby' => '',
+
+ 'match' => 'all',
+ 'status_is' => array(),
+ 'status_is_not' => array(),
+ 'product_includes' => array(),
+ 'product_excludes' => array(),
+ 'coupon_includes' => array(),
+ 'coupon_excludes' => array(),
+ 'tax_rate_includes' => array(),
+ 'tax_rate_excludes' => array(),
+ 'customer_type' => '',
+ 'payment_method_is' => array(),
+ 'payment_method_is_not' => array(),
+ 'category_includes' => array(),
)
);
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings.php b/plugins/woocommerce/src/Internal/Admin/Settings.php
index 27627531f62..c6b1cd22123 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings.php
@@ -84,7 +84,8 @@ class Settings {
}
/**
- * Get the titles of the installed payment gateways, for the payment method column of the Orders report.
+ * Get the payment gateways for the Orders report payment method filter and column: the installed
+ * ones by title, then any other gateway the stored orders were paid with, by id.
*
* @return array Payment gateway titles, keyed by gateway id.
*/
@@ -94,6 +95,10 @@ class Settings {
$titles[ $gateway->id ] = $gateway->get_method_title();
}
+ // A gateway that is no longer installed still has orders, so it stays in the filter under its id.
+ $stored = OrdersDataStore::get_all_payment_methods();
+ $titles += array_combine( $stored, $stored );
+
return $titles;
}
diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index ecf3856cf7e..6e54eeba09a 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -7,6 +7,7 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\API\Reports\Cache as ReportsCache;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;
@@ -1174,7 +1175,7 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
}
/**
- * @testdox Migration backfills the payment method on order and refund rows, then clears its cursor and the report cache.
+ * @testdox Migration backfills the payment method on order and refund rows, then clears its cursor, the report cache and the payment method filter cache.
*/
public function test_wc_update_1130_backfill_order_stats_payment_method(): void {
global $wpdb;
@@ -1212,6 +1213,10 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
$wpdb->query( "UPDATE {$stats_table} SET payment_method = NULL" );
$this->assertNull( $get_payment_method( $order->get_id() ), 'The order row should start without a payment method.' );
+ // A wc-admin page load before the backfill ends caches the payment method filter without the gateway.
+ wp_cache_delete( OrdersDataStore::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, 'woocommerce_analytics' );
+ $this->assertSame( array(), OrdersDataStore::get_all_payment_methods() );
+
$cache_key = 'wc_update_1130_payment_method_report';
$version_key = ReportsCache::VERSION_OPTION . '-transient-version';
$original_version = get_transient( $version_key );
@@ -1241,6 +1246,7 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
$this->assertSame( 'cheque', $get_payment_method( $refund->get_id() ), 'A refund takes the payment method of the order it refunds.' );
$this->assertNull( $get_payment_method( $child_order->get_id() ), 'A child order that is not a refund does not take the payment method of its parent.' );
$this->assertFalse( get_option( 'woocommerce_update_1130_last_payment_method_order_id' ), 'The cursor is cleared on completion.' );
+ $this->assertSame( array( 'cheque' ), OrdersDataStore::get_all_payment_methods(), 'The payment method filter cached before the backfill ended is cleared on completion.' );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/PaymentMethodTest.php b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/PaymentMethodTest.php
index e46685dbf2b..a57a86ed3bf 100644
--- a/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/PaymentMethodTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/API/Reports/Orders/PaymentMethodTest.php
@@ -3,7 +3,10 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Admin\API\Reports\Orders;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Query as OrdersQuery;
use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersStatsDataStore;
+use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\Query as OrdersStatsQuery;
use Automattic\WooCommerce\Enums\OrderStatus;
use WC_Helper_Order;
use WC_REST_Unit_Test_Case;
@@ -105,6 +108,41 @@ class PaymentMethodTest extends WC_REST_Unit_Test_Case {
}
}
+ /**
+ * @testdox The synced payment methods list each gateway once, and leave out a refund's repeat and an order without a gateway.
+ */
+ public function test_get_all_payment_methods_lists_each_synced_gateway_once(): void {
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $this->orders['bacs']->get_id(),
+ 'amount' => 10.00,
+ )
+ );
+ OrdersStatsDataStore::sync_order( $refund->get_id() );
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( '' );
+ $order->save();
+ OrdersStatsDataStore::sync_order( $order->get_id() );
+
+ wp_cache_delete( OrdersDataStore::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, 'woocommerce_analytics' );
+
+ $this->assertSame( array( 'bacs', 'cheque', 'cod' ), OrdersDataStore::get_all_payment_methods() );
+ }
+
+ /**
+ * @testdox Syncing an order paid with a new gateway adds it to the cached payment methods.
+ */
+ public function test_sync_adds_a_new_gateway_to_the_cached_payment_methods(): void {
+ OrdersDataStore::get_all_payment_methods();
+
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( 'a_new_gateway' );
+ $order->save();
+ OrdersStatsDataStore::sync_order( $order->get_id() );
+
+ $this->assertContains( 'a_new_gateway', wp_cache_get( OrdersDataStore::ORDERS_PAYMENT_METHODS_ALL_CACHE_KEY, 'woocommerce_analytics' ) );
+ }
+
/**
* @testdox Syncing an order and its refund before the update adds the payment method column still writes their rows.
*/
@@ -154,6 +192,120 @@ class PaymentMethodTest extends WC_REST_Unit_Test_Case {
}
}
+ /**
+ * @testdox payment_method_is limits the Orders report to the named gateways.
+ */
+ public function test_payment_method_is_limits_the_report(): void {
+ $order_ids = $this->get_report_order_ids( array( 'payment_method_is' => array( 'bacs', 'cod' ) ) );
+
+ $this->assertEqualsCanonicalizing(
+ array( $this->orders['bacs']->get_id(), $this->orders['cod']->get_id() ),
+ $order_ids
+ );
+ }
+
+ /**
+ * @testdox payment_method_is_not drops the named gateways from the Orders report.
+ */
+ public function test_payment_method_is_not_drops_the_named_gateways(): void {
+ $order_ids = $this->get_report_order_ids( array( 'payment_method_is_not' => array( 'bacs' ) ) );
+
+ $this->assertNotContains( $this->orders['bacs']->get_id(), $order_ids );
+ $this->assertContains( $this->orders['cheque']->get_id(), $order_ids );
+ $this->assertContains( $this->orders['cod']->get_id(), $order_ids );
+ }
+
+ /**
+ * @testdox payment_method_is_not keeps orders that carry no gateway, which are not the excluded one either.
+ */
+ public function test_payment_method_is_not_keeps_orders_without_a_gateway(): void {
+ global $wpdb;
+
+ $wpdb->update(
+ $wpdb->prefix . 'wc_order_stats',
+ array( 'payment_method' => null ),
+ array( 'order_id' => $this->orders['cod']->get_id() ),
+ array( '%s' ),
+ array( '%d' )
+ );
+
+ $order_ids = $this->get_report_order_ids( array( 'payment_method_is_not' => array( 'bacs' ) ) );
+
+ $this->assertContains( $this->orders['cod']->get_id(), $order_ids );
+ }
+
+ /**
+ * @testdox The include and exclude rules narrow together under match=any, so the table and the totals agree.
+ */
+ public function test_payment_method_rules_narrow_together_under_match_any(): void {
+ $args = array(
+ 'payment_method_is' => array( 'bacs' ),
+ 'payment_method_is_not' => array( 'cheque' ),
+ 'match' => 'any',
+ );
+
+ $this->assertSame( array( $this->orders['bacs']->get_id() ), $this->get_report_order_ids( $args ) );
+ $this->assertSame( 1, $this->get_stats_orders_count( $args ) );
+ }
+
+ /**
+ * @testdox The stats report totals honour the payment gateway filter, so they match the table.
+ */
+ public function test_stats_totals_honour_the_payment_method_filter(): void {
+ $unfiltered = $this->get_stats_orders_count( array() );
+ $filtered = $this->get_stats_orders_count( array( 'payment_method_is' => array( 'bacs' ) ) );
+
+ $this->assertSame( 3, $unfiltered );
+ $this->assertSame( 1, $filtered );
+ }
+
+ /**
+ * @testdox The Orders report endpoint applies both payment gateway rules and returns the gateway of each row.
+ */
+ public function test_orders_endpoint_applies_the_payment_method_rules(): void {
+ $rows = $this->get_rest_report( '/wc-analytics/reports/orders' );
+
+ $this->assertSame( array( $this->orders['bacs']->get_id() ), wp_list_pluck( $rows, 'order_id' ) );
+ $this->assertSame( 'bacs', $rows[0]['payment_method'] );
+ }
+
+ /**
+ * @testdox The Orders stats endpoint applies both payment gateway rules to its totals.
+ */
+ public function test_orders_stats_endpoint_applies_the_payment_method_rules(): void {
+ $report = $this->get_rest_report( '/wc-analytics/reports/orders/stats' );
+
+ $this->assertSame( 1, $report['totals']['orders_count'] );
+ }
+
+ /**
+ * Request a report endpoint as an administrator, with rules that leave only the bacs order.
+ *
+ * @param string $endpoint REST route of the report.
+ * @return array
+ */
+ private function get_rest_report( string $endpoint ): array {
+ wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) );
+
+ $request = new WP_REST_Request( 'GET', $endpoint );
+ $request->set_query_params(
+ array_merge(
+ $this->get_period_args(),
+ // Comma separated, as the client sends them, so a rule the endpoint does not parse matches no gateway.
+ // The cheque order is in neither list, so the result changes when either rule is not applied.
+ array(
+ 'payment_method_is' => 'bacs,cod',
+ 'payment_method_is_not' => 'cod,paypal',
+ )
+ )
+ );
+ $response = rest_do_request( $request );
+
+ $this->assertSame( 200, $response->get_status() );
+
+ return $response->get_data();
+ }
+
/**
* Read the payment method stored for an order in the stats table.
*
@@ -171,6 +323,30 @@ class PaymentMethodTest extends WC_REST_Unit_Test_Case {
);
}
+ /**
+ * Run the Orders report and return the order ids it holds.
+ *
+ * @param array $args Query arguments on top of the reporting period.
+ * @return array
+ */
+ private function get_report_order_ids( array $args ): array {
+ $query = new OrdersQuery( array_merge( $this->get_period_args(), $args ) );
+
+ return wp_list_pluck( $query->get_data()->data, 'order_id' );
+ }
+
+ /**
+ * Run the Orders stats report and return its order count.
+ *
+ * @param array $args Query arguments on top of the reporting period.
+ * @return int
+ */
+ private function get_stats_orders_count( array $args ): int {
+ $query = new OrdersStatsQuery( array_merge( $this->get_period_args(), $args ) );
+
+ return (int) $query->get_data()->totals->orders_count;
+ }
+
/**
* The reporting period and cache settings shared by every query in this test.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php
index acce18d7ed0..c04b69f3571 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/SettingsTest.php
@@ -3,7 +3,9 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\Admin;
+use Automattic\WooCommerce\Internal\Admin\Schedulers\OrdersScheduler;
use Automattic\WooCommerce\Internal\Admin\Settings;
+use WC_Helper_Order;
use WC_REST_Setting_Options_Controller;
use WC_Unit_Test_Case;
use WP_REST_Request;
@@ -334,21 +336,30 @@ class SettingsTest extends WC_Unit_Test_Case {
}
/**
- * @testdox The preloaded component settings list the installed payment gateways by title.
+ * @testdox The preloaded component settings list the installed payment gateways by title, then any other gateway the stored orders were paid with, by id.
*/
- public function test_component_settings_expose_payment_gateway_titles(): void {
+ public function test_component_settings_include_payment_gateways_that_are_no_longer_installed(): void {
+ foreach ( array( 'bacs', 'a_removed_gateway', '' ) as $payment_method ) {
+ $order = WC_Helper_Order::create_order();
+ $order->set_payment_method( $payment_method );
+ $order->save();
+ OrdersScheduler::import( $order->get_id() );
+ }
// Payment gateways are only injected on wc-admin pages; the base tearDown unsets the current screen.
set_current_screen( 'dashboard' );
$_GET['page'] = 'wc-admin'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
try {
- $settings = $this->sut->add_component_settings( array() );
+ $gateways = $this->sut->add_component_settings( array() )['paymentGateways'];
} finally {
unset( $_GET['page'] );
}
- $this->assertSame( array_keys( WC()->payment_gateways()->payment_gateways() ), array_keys( $settings['paymentGateways'] ), 'Every installed gateway should be listed, in their order.' );
- $this->assertSame( 'Direct bank transfer', $settings['paymentGateways']['bacs'], 'An installed gateway should be listed by its title.' );
+ $installed = array_keys( WC()->payment_gateways()->payment_gateways() );
+ $this->assertSame( $installed, array_slice( array_keys( $gateways ), 0, count( $installed ) ), 'The installed gateways should stay first, in their order.' );
+ $this->assertSame( 'Direct bank transfer', $gateways['bacs'], 'An installed gateway should keep its title.' );
+ $this->assertSame( 'a_removed_gateway', $gateways['a_removed_gateway'], 'A gateway that is gone should be listed under its id.' );
+ $this->assertArrayNotHasKey( '', $gateways, 'An order placed without a gateway should not add an empty option.' );
}
/**