From 361a355dda706947fc1e7b7986f3920c71140912 Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Mon, 18 Nov 2024 13:11:22 +0100
Subject: [PATCH 01/10] feat(wc): duplicate orders admin notice
---
.../class-woocommerce-connection.php | 1 +
.../class-woocommerce-duplicate-orders.php | 137 ++++++++++++++++++
2 files changed, 138 insertions(+)
create mode 100644 includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php
index fc1c5835e0..48f1f7ce36 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php
@@ -35,6 +35,7 @@ public static function init() {
include_once __DIR__ . '/class-woocommerce-cover-fees.php';
include_once __DIR__ . '/class-woocommerce-order-utm.php';
include_once __DIR__ . '/class-woocommerce-products.php';
+ include_once __DIR__ . '/class-woocommerce-duplicate-orders.php';
\add_action( 'admin_init', [ __CLASS__, 'disable_woocommerce_setup' ] );
\add_filter( 'option_woocommerce_subscriptions_allow_switching', [ __CLASS__, 'force_allow_subscription_switching' ], 10, 2 );
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
new file mode 100644
index 0000000000..256ac843de
--- /dev/null
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -0,0 +1,137 @@
+posts} AS p
+ JOIN {$wpdb->postmeta} AS pm_email
+ ON p.ID = pm_email.post_id
+ JOIN {$wpdb->postmeta} AS pm_amount
+ ON p.ID = pm_amount.post_id
+ LEFT JOIN {$wpdb->postmeta} AS pm_renewal
+ ON p.ID = pm_renewal.post_id AND pm_renewal.meta_key = '_subscription_renewal'
+ WHERE p.post_type = 'shop_order'
+ AND pm_email.meta_key = '_billing_email'
+ AND pm_amount.meta_key = '_order_total'
+ AND CAST(pm_amount.meta_value AS DECIMAL(10,2)) > 0.00 -- Exclude transactions with 0.00 amount
+ AND pm_renewal.post_id IS NULL
+ AND EXISTS (
+ SELECT 1
+ FROM {$wpdb->posts} AS p2
+ WHERE p2.post_type = 'shop_order'
+ AND pm_email.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_billing_email')
+ AND pm_amount.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_order_total')
+ AND ABS(TIMESTAMPDIFF(MINUTE, p2.post_date, p.post_date)) <= $series_interval
+ AND p2.ID != p.ID
+ )
+ GROUP BY pm_email.meta_value, DATE(p.post_date), pm_amount.meta_value
+ HAVING COUNT(*) > 1 -- Ensures only duplicates are included
+ ";
+ return $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
+ }
+
+ /**
+ * Add an admin notice about the detected order series.
+ */
+ public static function check_for_order_series() {
+ $order_series = self::detect_order_series();
+ if ( empty( $order_series ) ) {
+ return;
+ }
+ update_option( self::DUPLICATED_ORDERS_OPTION_NAME, $order_series );
+ }
+
+ /**
+ * Display an admin notice if duplicate orders are found.
+ */
+ public static function display_admin_notice() {
+ if ( ! function_exists( 'wc_price' ) ) {
+ return;
+ }
+ $order_series = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
+ ?>
+
+
+
+
+
+
+
+ -
+
+
+ $order_id ) :
+ $order_url = admin_url( 'post.php?post=' . intval( $order_id ) . '&action=edit' );
+ ?>
+
+
+
+
+
+
+
Date: Tue, 19 Nov 2024 15:07:40 +0100
Subject: [PATCH 02/10] feat: handle HPOS
---
.../class-woocommerce-duplicate-orders.php | 100 ++++++++++++------
1 file changed, 68 insertions(+), 32 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 256ac843de..38065e16f5 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -39,36 +39,72 @@ public static function init() {
*/
public static function detect_order_series( $series_interval = 10 ) {
global $wpdb;
- $query = "
- SELECT
- pm_email.meta_value AS email,
- DATE(p.post_date) AS date,
- pm_amount.meta_value AS amount,
- GROUP_CONCAT(p.ID) AS ids
- FROM {$wpdb->posts} AS p
- JOIN {$wpdb->postmeta} AS pm_email
- ON p.ID = pm_email.post_id
- JOIN {$wpdb->postmeta} AS pm_amount
- ON p.ID = pm_amount.post_id
- LEFT JOIN {$wpdb->postmeta} AS pm_renewal
- ON p.ID = pm_renewal.post_id AND pm_renewal.meta_key = '_subscription_renewal'
- WHERE p.post_type = 'shop_order'
- AND pm_email.meta_key = '_billing_email'
- AND pm_amount.meta_key = '_order_total'
- AND CAST(pm_amount.meta_value AS DECIMAL(10,2)) > 0.00 -- Exclude transactions with 0.00 amount
- AND pm_renewal.post_id IS NULL
- AND EXISTS (
- SELECT 1
- FROM {$wpdb->posts} AS p2
- WHERE p2.post_type = 'shop_order'
- AND pm_email.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_billing_email')
- AND pm_amount.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_order_total')
- AND ABS(TIMESTAMPDIFF(MINUTE, p2.post_date, p.post_date)) <= $series_interval
- AND p2.ID != p.ID
- )
- GROUP BY pm_email.meta_value, DATE(p.post_date), pm_amount.meta_value
- HAVING COUNT(*) > 1 -- Ensures only duplicates are included
- ";
+
+ $is_hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
+
+ if ( $is_hpos_enabled ) {
+ $query = "
+ SELECT
+ o_billing.email AS email,
+ DATE(o.date_created_gmt) AS date,
+ o.total_amount AS amount,
+ GROUP_CONCAT(o.id) AS ids
+ FROM {$wpdb->prefix}wc_orders AS o
+ JOIN {$wpdb->prefix}wc_order_addresses AS o_billing
+ ON o.id = o_billing.order_id AND o_billing.address_type = 'billing'
+ LEFT JOIN {$wpdb->prefix}wc_orders_meta AS o_meta
+ ON o.id = o_meta.order_id AND o_meta.meta_key = '_subscription_renewal'
+ WHERE o_billing.email IS NOT NULL
+ AND o.total_amount > 0.00
+ AND o.status = 'wc-completed'
+ AND o_meta.order_id IS NULL
+ AND EXISTS (
+ SELECT 1
+ FROM {$wpdb->prefix}wc_orders AS o2
+ JOIN {$wpdb->prefix}wc_order_addresses AS o2_billing
+ ON o2.id = o2_billing.order_id AND o2_billing.address_type = 'billing'
+ WHERE o2_billing.email = o_billing.email
+ AND o2.total_amount = o.total_amount
+ AND ABS(TIMESTAMPDIFF(MINUTE, o2.date_created_gmt, o.date_created_gmt)) <= $series_interval
+ AND o2.id != o.id
+ )
+ GROUP BY o_billing.email, DATE(o.date_created_gmt), o.total_amount
+ HAVING COUNT(*) > 1 -- Ensures only duplicates are included
+ ";
+ } else {
+ $query = "
+ SELECT
+ pm_email.meta_value AS email,
+ DATE(p.post_date) AS date,
+ pm_amount.meta_value AS amount,
+ GROUP_CONCAT(p.ID) AS ids
+ FROM {$wpdb->posts} AS p
+ JOIN {$wpdb->postmeta} AS pm_email
+ ON p.ID = pm_email.post_id
+ JOIN {$wpdb->postmeta} AS pm_amount
+ ON p.ID = pm_amount.post_id
+ LEFT JOIN {$wpdb->postmeta} AS pm_renewal
+ ON p.ID = pm_renewal.post_id AND pm_renewal.meta_key = '_subscription_renewal'
+ WHERE p.post_type = 'shop_order'
+ AND p.post_status = 'wc-completed'
+ AND pm_email.meta_key = '_billing_email'
+ AND pm_amount.meta_key = '_order_total'
+ AND CAST(pm_amount.meta_value AS DECIMAL(10,2)) > 0.00
+ AND pm_renewal.post_id IS NULL
+ AND EXISTS (
+ SELECT 1
+ FROM {$wpdb->posts} AS p2
+ WHERE p2.post_type = 'shop_order'
+ AND pm_email.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_billing_email')
+ AND pm_amount.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_order_total')
+ AND ABS(TIMESTAMPDIFF(MINUTE, p2.post_date, p.post_date)) <= $series_interval
+ AND p2.ID != p.ID
+ )
+ GROUP BY pm_email.meta_value, DATE(p.post_date), pm_amount.meta_value
+ HAVING COUNT(*) > 1 -- Ensures only duplicates are included
+ ";
+ }
+
return $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
}
@@ -118,12 +154,12 @@ public static function display_admin_notice() {
$order_list = ob_get_clean();
printf(
- /* translators: 1: customer email, 2: order amount, 3: order IDs */
+ /* translators: 1: customer email, 2: order amount, 3: orders date, 4: order IDs */
wp_kses_post( __( 'Customer %1$s made multiple orders of %2$s on %3$s. Orders: %4$s.', 'newspack-plugin' ) ),
wp_kses_post( $customer_email ),
wp_kses_post( \wc_price( $order_series['amount'] ) ),
esc_html( date_i18n( get_option( 'date_format' ), strtotime( $order_series['date'] ) ) ),
- wp_kses_post( $order_list )
+ wp_kses_post( trim( $order_list ) )
);
?>
From 4353272d4a33c6ba09f19fd3075a0f83e9a1c4e3 Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Tue, 19 Nov 2024 15:44:48 +0100
Subject: [PATCH 03/10] feat: handle order series dismissal
---
.../class-woocommerce-duplicate-orders.php | 55 ++++++++++++++++---
1 file changed, 47 insertions(+), 8 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 38065e16f5..345f2ddbaa 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -16,13 +16,14 @@ class WooCommerce_Duplicate_Orders {
const CRON_HOOK_NAME = 'newspack_wc_check_order_series';
const ADMIN_NOTICE_TRANSIENT_NAME = 'newspack_wc_check_order_series_admin_notice';
const DUPLICATED_ORDERS_OPTION_NAME = 'newspack_wc_order_series';
+ const DISMISSED_DUPLICATE_ORDER_META_NAME = '_newspack_dismissed_duplicate';
/**
* Initialize.
*
* @codeCoverageIgnore
*/
- public static function init() {
+ public static function init(): void {
if ( ! wp_next_scheduled( self::CRON_HOOK_NAME ) ) {
wp_schedule_event( time(), 'daily', self::CRON_HOOK_NAME );
}
@@ -30,6 +31,13 @@ public static function init() {
add_action( 'admin_notices', [ __CLASS__, 'display_admin_notice' ] );
}
+ /**
+ * Is this site using HPOS?
+ */
+ private static function is_using_hpos(): bool {
+ return class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
+ }
+
/**
* Detect duplicate orders.
* An order series will be detected if it the same amount, same day, from the same customer,
@@ -37,12 +45,10 @@ public static function init() {
*
* @param int $series_interval The interval to consider for matching transactions.
*/
- public static function detect_order_series( $series_interval = 10 ) {
+ public static function detect_order_series( $series_interval = 10 ): array {
global $wpdb;
- $is_hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
-
- if ( $is_hpos_enabled ) {
+ if ( self::is_using_hpos() ) {
$query = "
SELECT
o_billing.email AS email,
@@ -58,6 +64,11 @@ public static function detect_order_series( $series_interval = 10 ) {
AND o.total_amount > 0.00
AND o.status = 'wc-completed'
AND o_meta.order_id IS NULL
+ AND o.id NOT IN (
+ SELECT order_id
+ FROM {$wpdb->prefix}wc_orders_meta
+ WHERE meta_key = '" . self::DISMISSED_DUPLICATE_ORDER_META_NAME . "' AND meta_value = '1'
+ )
AND EXISTS (
SELECT 1
FROM {$wpdb->prefix}wc_orders AS o2
@@ -88,6 +99,11 @@ public static function detect_order_series( $series_interval = 10 ) {
WHERE p.post_type = 'shop_order'
AND p.post_status = 'wc-completed'
AND pm_email.meta_key = '_billing_email'
+ AND p.ID NOT IN (
+ SELECT post_id
+ FROM {$wpdb->postmeta}
+ WHERE meta_key = '" . self::DISMISSED_DUPLICATE_ORDER_META_NAME . "' AND meta_value = '1'
+ )
AND pm_amount.meta_key = '_order_total'
AND CAST(pm_amount.meta_value AS DECIMAL(10,2)) > 0.00
AND pm_renewal.post_id IS NULL
@@ -111,7 +127,7 @@ public static function detect_order_series( $series_interval = 10 ) {
/**
* Add an admin notice about the detected order series.
*/
- public static function check_for_order_series() {
+ public static function check_for_order_series(): void {
$order_series = self::detect_order_series();
if ( empty( $order_series ) ) {
return;
@@ -122,7 +138,7 @@ public static function check_for_order_series() {
/**
* Display an admin notice if duplicate orders are found.
*/
- public static function display_admin_notice() {
+ public static function display_admin_notice(): void {
if ( ! function_exists( 'wc_price' ) ) {
return;
}
@@ -135,7 +151,9 @@ public static function display_admin_notice() {
- -
+
-
+
+
@@ -161,12 +179,33 @@ public static function display_admin_notice() {
esc_html( date_i18n( get_option( 'date_format' ), strtotime( $order_series['date'] ) ) ),
wp_kses_post( trim( $order_list ) )
);
+
+ $order_series_id = implode( '-', $order_ids );
?>
+
+
add_meta_data( self::DISMISSED_DUPLICATE_ORDER_META_NAME, 1 );
+ $wc_order->save();
+ }
+ }
+ self::check_for_order_series();
+ // Refresh the page to reflect changes.
+ wp_safe_redirect( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : admin_url() );
+ exit;
+ }
}
}
From a7b9eeb9d5e4f04e576ce0a3b550de6b85224ba0 Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Tue, 19 Nov 2024 15:51:08 +0100
Subject: [PATCH 04/10] feat: better UX with a details element
---
.../class-woocommerce-duplicate-orders.php | 80 ++++++++++---------
1 file changed, 41 insertions(+), 39 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 345f2ddbaa..3925245f89 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -146,50 +146,52 @@ public static function display_admin_notice(): void {
?>
-
-
-
-
-
- -
-
+
+
+
+
+
+ printf(
+ /* translators: 1: customer email, 2: order amount, 3: orders date, 4: order IDs */
+ wp_kses_post( __( 'Customer %1$s made multiple orders of %2$s on %3$s. Orders: %4$s.', 'newspack-plugin' ) ),
+ wp_kses_post( $customer_email ),
+ wp_kses_post( \wc_price( $order_series['amount'] ) ),
+ esc_html( date_i18n( get_option( 'date_format' ), strtotime( $order_series['date'] ) ) ),
+ wp_kses_post( trim( $order_list ) )
+ );
+
+ $order_series_id = implode( '-', $order_ids );
+ ?>
+
+
+
+
+
+
Date: Mon, 25 Nov 2024 12:26:43 +0100
Subject: [PATCH 05/10] feat: iterate orders instead of using a direct DB query
---
.../class-woocommerce-duplicate-orders.php | 137 +++++++-----------
1 file changed, 53 insertions(+), 84 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 3925245f89..d054f78208 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -31,104 +31,73 @@ public static function init(): void {
add_action( 'admin_notices', [ __CLASS__, 'display_admin_notice' ] );
}
- /**
- * Is this site using HPOS?
- */
- private static function is_using_hpos(): bool {
- return class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
- }
-
/**
* Detect duplicate orders.
* An order series will be detected if it the same amount, same day, from the same customer,
* in the time span provided in the argument (in minutes).
*
- * @param int $series_interval The interval to consider for matching transactions.
+ * @param number $cutoff_time The cutoff time in the past (how many seconds ago).
*/
- public static function detect_order_series( $series_interval = 10 ): array {
- global $wpdb;
-
- if ( self::is_using_hpos() ) {
- $query = "
- SELECT
- o_billing.email AS email,
- DATE(o.date_created_gmt) AS date,
- o.total_amount AS amount,
- GROUP_CONCAT(o.id) AS ids
- FROM {$wpdb->prefix}wc_orders AS o
- JOIN {$wpdb->prefix}wc_order_addresses AS o_billing
- ON o.id = o_billing.order_id AND o_billing.address_type = 'billing'
- LEFT JOIN {$wpdb->prefix}wc_orders_meta AS o_meta
- ON o.id = o_meta.order_id AND o_meta.meta_key = '_subscription_renewal'
- WHERE o_billing.email IS NOT NULL
- AND o.total_amount > 0.00
- AND o.status = 'wc-completed'
- AND o_meta.order_id IS NULL
- AND o.id NOT IN (
- SELECT order_id
- FROM {$wpdb->prefix}wc_orders_meta
- WHERE meta_key = '" . self::DISMISSED_DUPLICATE_ORDER_META_NAME . "' AND meta_value = '1'
- )
- AND EXISTS (
- SELECT 1
- FROM {$wpdb->prefix}wc_orders AS o2
- JOIN {$wpdb->prefix}wc_order_addresses AS o2_billing
- ON o2.id = o2_billing.order_id AND o2_billing.address_type = 'billing'
- WHERE o2_billing.email = o_billing.email
- AND o2.total_amount = o.total_amount
- AND ABS(TIMESTAMPDIFF(MINUTE, o2.date_created_gmt, o.date_created_gmt)) <= $series_interval
- AND o2.id != o.id
- )
- GROUP BY o_billing.email, DATE(o.date_created_gmt), o.total_amount
- HAVING COUNT(*) > 1 -- Ensures only duplicates are included
- ";
- } else {
- $query = "
- SELECT
- pm_email.meta_value AS email,
- DATE(p.post_date) AS date,
- pm_amount.meta_value AS amount,
- GROUP_CONCAT(p.ID) AS ids
- FROM {$wpdb->posts} AS p
- JOIN {$wpdb->postmeta} AS pm_email
- ON p.ID = pm_email.post_id
- JOIN {$wpdb->postmeta} AS pm_amount
- ON p.ID = pm_amount.post_id
- LEFT JOIN {$wpdb->postmeta} AS pm_renewal
- ON p.ID = pm_renewal.post_id AND pm_renewal.meta_key = '_subscription_renewal'
- WHERE p.post_type = 'shop_order'
- AND p.post_status = 'wc-completed'
- AND pm_email.meta_key = '_billing_email'
- AND p.ID NOT IN (
- SELECT post_id
- FROM {$wpdb->postmeta}
- WHERE meta_key = '" . self::DISMISSED_DUPLICATE_ORDER_META_NAME . "' AND meta_value = '1'
- )
- AND pm_amount.meta_key = '_order_total'
- AND CAST(pm_amount.meta_value AS DECIMAL(10,2)) > 0.00
- AND pm_renewal.post_id IS NULL
- AND EXISTS (
- SELECT 1
- FROM {$wpdb->posts} AS p2
- WHERE p2.post_type = 'shop_order'
- AND pm_email.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_billing_email')
- AND pm_amount.meta_value = (SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = p2.ID AND meta_key = '_order_total')
- AND ABS(TIMESTAMPDIFF(MINUTE, p2.post_date, p.post_date)) <= $series_interval
- AND p2.ID != p.ID
- )
- GROUP BY pm_email.meta_value, DATE(p.post_date), pm_amount.meta_value
- HAVING COUNT(*) > 1 -- Ensures only duplicates are included
- ";
+ public static function get_order_series( $cutoff_time = MONTH_IN_SECONDS ): array {
+ $orders = wc_get_orders(
+ [
+ 'limit' => -1,
+ 'status' => [ 'wc-completed' ],
+ 'date_completed' => '<' . ( time() - $cutoff_time ),
+ ]
+ );
+
+ $order_series = [];
+
+ foreach ( $orders as $order ) {
+ $email = $order->get_billing_email();
+ $amount = $order->get_total();
+ $date = $order->get_date_created()->date( 'Y-m-d' );
+
+ if ( \wcs_order_contains_renewal( $order ) || \wcs_order_contains_resubscribe( $order ) ) {
+ continue;
+ }
+
+ if ( ! isset( $order_series[ $email ] ) ) {
+ $order_series[ $email ] = [];
+ }
+
+ if ( ! isset( $order_series[ $email ][ $amount ] ) ) {
+ $order_series[ $email ][ $amount ] = [];
+ }
+
+ if ( ! isset( $order_series[ $email ][ $amount ][ $date ] ) ) {
+ $order_series[ $email ][ $amount ][ $date ] = [];
+ }
+
+ $order_series[ $email ][ $amount ][ $date ][] = $order->get_id();
+ }
+
+ $duplicates = [];
+
+ foreach ( $order_series as $email => $amounts ) {
+ foreach ( $amounts as $amount => $dates ) {
+ foreach ( $dates as $date => $order_ids ) {
+ if ( count( $order_ids ) > 1 ) {
+ $duplicates[] = [
+ 'email' => $email,
+ 'amount' => $amount,
+ 'date' => $date,
+ 'ids' => implode( ',', $order_ids ),
+ ];
+ }
+ }
+ }
}
- return $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery
+ return $duplicates;
}
/**
* Add an admin notice about the detected order series.
*/
public static function check_for_order_series(): void {
- $order_series = self::detect_order_series();
+ $order_series = self::get_order_series();
if ( empty( $order_series ) ) {
return;
}
From 3468af6af6b92f2207546264ae57ab225d58ee27 Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Thu, 28 Nov 2024 11:04:34 +0100
Subject: [PATCH 06/10] chore: unify wording
---
.../class-woocommerce-duplicate-orders.php | 70 +++++++++----------
1 file changed, 35 insertions(+), 35 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index d054f78208..ef2e8315c6 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -13,9 +13,9 @@
* Adds an admin notice when possibly duplicated orders are detected.
*/
class WooCommerce_Duplicate_Orders {
- const CRON_HOOK_NAME = 'newspack_wc_check_order_series';
- const ADMIN_NOTICE_TRANSIENT_NAME = 'newspack_wc_check_order_series_admin_notice';
- const DUPLICATED_ORDERS_OPTION_NAME = 'newspack_wc_order_series';
+ const CRON_HOOK_NAME = 'newspack_wc_check_order_duplicates';
+ const ADMIN_NOTICE_TRANSIENT_NAME = 'newspack_wc_check_order_duplicates_admin_notice';
+ const DUPLICATED_ORDERS_OPTION_NAME = 'newspack_wc_order_duplicates';
const DISMISSED_DUPLICATE_ORDER_META_NAME = '_newspack_dismissed_duplicate';
/**
@@ -27,27 +27,26 @@ public static function init(): void {
if ( ! wp_next_scheduled( self::CRON_HOOK_NAME ) ) {
wp_schedule_event( time(), 'daily', self::CRON_HOOK_NAME );
}
- add_action( self::CRON_HOOK_NAME, [ __CLASS__, 'check_for_order_series' ] );
+ add_action( self::CRON_HOOK_NAME, [ __CLASS__, 'check_for_order_duplicates' ] );
add_action( 'admin_notices', [ __CLASS__, 'display_admin_notice' ] );
}
/**
* Detect duplicate orders.
- * An order series will be detected if it the same amount, same day, from the same customer,
- * in the time span provided in the argument (in minutes).
+ * Duplicates will be detected if it the same amount, same day, from the same customer.
*
* @param number $cutoff_time The cutoff time in the past (how many seconds ago).
*/
- public static function get_order_series( $cutoff_time = MONTH_IN_SECONDS ): array {
+ public static function get_order_duplicates( $cutoff_time = MONTH_IN_SECONDS ): array {
$orders = wc_get_orders(
[
'limit' => -1,
'status' => [ 'wc-completed' ],
- 'date_completed' => '<' . ( time() - $cutoff_time ),
+ 'date_completed' => '>' . ( time() - $cutoff_time ),
]
);
- $order_series = [];
+ $order_duplicates = [];
foreach ( $orders as $order ) {
$email = $order->get_billing_email();
@@ -58,28 +57,29 @@ public static function get_order_series( $cutoff_time = MONTH_IN_SECONDS ): arra
continue;
}
- if ( ! isset( $order_series[ $email ] ) ) {
- $order_series[ $email ] = [];
+ if ( ! isset( $order_duplicates[ $email ] ) ) {
+ $order_duplicates[ $email ] = [];
}
- if ( ! isset( $order_series[ $email ][ $amount ] ) ) {
- $order_series[ $email ][ $amount ] = [];
+ if ( ! isset( $order_duplicates[ $email ][ $amount ] ) ) {
+ $order_duplicates[ $email ][ $amount ] = [];
}
- if ( ! isset( $order_series[ $email ][ $amount ][ $date ] ) ) {
- $order_series[ $email ][ $amount ][ $date ] = [];
+ if ( ! isset( $order_duplicates[ $email ][ $amount ][ $date ] ) ) {
+ $order_duplicates[ $email ][ $amount ][ $date ] = [];
}
- $order_series[ $email ][ $amount ][ $date ][] = $order->get_id();
+ $order_duplicates[ $email ][ $amount ][ $date ][] = $order->get_id();
}
- $duplicates = [];
+ $results = [];
- foreach ( $order_series as $email => $amounts ) {
+ foreach ( $order_duplicates as $email => $amounts ) {
foreach ( $amounts as $amount => $dates ) {
foreach ( $dates as $date => $order_ids ) {
if ( count( $order_ids ) > 1 ) {
- $duplicates[] = [
+ sort( $order_ids );
+ $results[] = [
'email' => $email,
'amount' => $amount,
'date' => $date,
@@ -90,18 +90,18 @@ public static function get_order_series( $cutoff_time = MONTH_IN_SECONDS ): arra
}
}
- return $duplicates;
+ return $results;
}
/**
- * Add an admin notice about the detected order series.
+ * Check for duplicate orders and save the result in an option.
*/
- public static function check_for_order_series(): void {
- $order_series = self::get_order_series();
- if ( empty( $order_series ) ) {
+ public static function check_for_order_duplicates(): void {
+ $order_duplicates = self::get_order_duplicates();
+ if ( empty( $order_duplicates ) ) {
return;
}
- update_option( self::DUPLICATED_ORDERS_OPTION_NAME, $order_series );
+ update_option( self::DUPLICATED_ORDERS_OPTION_NAME, $order_duplicates );
}
/**
@@ -111,7 +111,7 @@ public static function display_admin_notice(): void {
if ( ! function_exists( 'wc_price' ) ) {
return;
}
- $order_series = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
+ $order_duplicates = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
?>
@@ -120,19 +120,19 @@ public static function display_admin_notice(): void {
-
+
-
-
+
$order_id ) :
$order_url = admin_url( 'post.php?post=' . intval( $order_id ) . '&action=edit' );
?>
@@ -145,17 +145,17 @@ public static function display_admin_notice(): void {
/* translators: 1: customer email, 2: order amount, 3: orders date, 4: order IDs */
wp_kses_post( __( 'Customer %1$s made multiple orders of %2$s on %3$s. Orders: %4$s.', 'newspack-plugin' ) ),
wp_kses_post( $customer_email ),
- wp_kses_post( \wc_price( $order_series['amount'] ) ),
- esc_html( date_i18n( get_option( 'date_format' ), strtotime( $order_series['date'] ) ) ),
+ wp_kses_post( \wc_price( $order_duplicates['amount'] ) ),
+ esc_html( date_i18n( get_option( 'date_format' ), strtotime( $order_duplicates['date'] ) ) ),
wp_kses_post( trim( $order_list ) )
);
- $order_series_id = implode( '-', $order_ids );
+ $order_duplicates_id = implode( '-', $order_ids );
?>
@@ -172,7 +172,7 @@ public static function display_admin_notice(): void {
$wc_order->save();
}
}
- self::check_for_order_series();
+ self::check_for_order_duplicates();
// Refresh the page to reflect changes.
wp_safe_redirect( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : admin_url() );
exit;
From 224b24e95a2ad73d362ff3129143b9069b461609 Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Thu, 28 Nov 2024 12:05:19 +0100
Subject: [PATCH 07/10] fix: handle the order meta
---
.../woocommerce/class-woocommerce-duplicate-orders.php | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index ef2e8315c6..2b7c0bcdda 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -53,7 +53,11 @@ public static function get_order_duplicates( $cutoff_time = MONTH_IN_SECONDS ):
$amount = $order->get_total();
$date = $order->get_date_created()->date( 'Y-m-d' );
- if ( \wcs_order_contains_renewal( $order ) || \wcs_order_contains_resubscribe( $order ) ) {
+ if (
+ \wcs_order_contains_renewal( $order ) ||
+ \wcs_order_contains_resubscribe( $order ) ||
+ $order->get_meta( self::DISMISSED_DUPLICATE_ORDER_META_NAME )
+ ) {
continue;
}
From 0fa853eb1487f0b75efa9ab5ca5c47d8aac50185 Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Thu, 28 Nov 2024 12:32:04 +0100
Subject: [PATCH 08/10] feat: CLI tool
---
.../class-woocommerce-duplicate-orders.php | 95 ++++++++++++++-----
1 file changed, 71 insertions(+), 24 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 2b7c0bcdda..2798f9b643 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -16,7 +16,7 @@ class WooCommerce_Duplicate_Orders {
const CRON_HOOK_NAME = 'newspack_wc_check_order_duplicates';
const ADMIN_NOTICE_TRANSIENT_NAME = 'newspack_wc_check_order_duplicates_admin_notice';
const DUPLICATED_ORDERS_OPTION_NAME = 'newspack_wc_order_duplicates';
- const DISMISSED_DUPLICATE_ORDER_META_NAME = '_newspack_dismissed_duplicate';
+ const DISMISSED_DUPLICATES_OPTION_NAME = 'newspack_wc_order_duplicates_dismissed';
/**
* Initialize.
@@ -29,6 +29,10 @@ public static function init(): void {
}
add_action( self::CRON_HOOK_NAME, [ __CLASS__, 'check_for_order_duplicates' ] );
add_action( 'admin_notices', [ __CLASS__, 'display_admin_notice' ] );
+
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ \WP_CLI::add_command( 'newspack detect-order-duplicates', [ __CLASS__, 'cli_upsert_order_duplicates' ] );
+ }
}
/**
@@ -37,7 +41,7 @@ public static function init(): void {
*
* @param number $cutoff_time The cutoff time in the past (how many seconds ago).
*/
- public static function get_order_duplicates( $cutoff_time = MONTH_IN_SECONDS ): array {
+ private static function get_order_duplicates( $cutoff_time ): array {
$orders = wc_get_orders(
[
'limit' => -1,
@@ -53,11 +57,7 @@ public static function get_order_duplicates( $cutoff_time = MONTH_IN_SECONDS ):
$amount = $order->get_total();
$date = $order->get_date_created()->date( 'Y-m-d' );
- if (
- \wcs_order_contains_renewal( $order ) ||
- \wcs_order_contains_resubscribe( $order ) ||
- $order->get_meta( self::DISMISSED_DUPLICATE_ORDER_META_NAME )
- ) {
+ if ( \wcs_order_contains_renewal( $order ) || \wcs_order_contains_resubscribe( $order ) ) {
continue;
}
@@ -83,11 +83,12 @@ public static function get_order_duplicates( $cutoff_time = MONTH_IN_SECONDS ):
foreach ( $dates as $date => $order_ids ) {
if ( count( $order_ids ) > 1 ) {
sort( $order_ids );
- $results[] = [
+ $ids = implode( ',', $order_ids );
+ $results[ $ids ] = [
'email' => $email,
'amount' => $amount,
'date' => $date,
- 'ids' => implode( ',', $order_ids ),
+ 'ids' => $ids,
];
}
}
@@ -99,13 +100,29 @@ public static function get_order_duplicates( $cutoff_time = MONTH_IN_SECONDS ):
/**
* Check for duplicate orders and save the result in an option.
+ *
+ * @param number $cutoff_time The cutoff time in the past (how many seconds ago).
+ * @param bool $save Whether to save the result as the option.
+ * @param bool $upsert Whether to upsert the option (merge with existing).
*/
- public static function check_for_order_duplicates(): void {
- $order_duplicates = self::get_order_duplicates();
+ public static function check_for_order_duplicates( $cutoff_time = MONTH_IN_SECONDS, $save = false, $upsert = true ): array {
+ $order_duplicates = self::get_order_duplicates( $cutoff_time );
if ( empty( $order_duplicates ) ) {
- return;
+ return [];
}
- update_option( self::DUPLICATED_ORDERS_OPTION_NAME, $order_duplicates );
+ if ( $save ) {
+ if ( $upsert ) {
+ $existing_order_duplicates = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
+ foreach ( $existing_order_duplicates as $key => $value ) {
+ if ( isset( $order_duplicates[ $key ] ) ) {
+ continue;
+ }
+ $order_duplicates[ $key ] = $value;
+ }
+ }
+ update_option( self::DUPLICATED_ORDERS_OPTION_NAME, $order_duplicates );
+ }
+ return $order_duplicates;
}
/**
@@ -115,7 +132,8 @@ public static function display_admin_notice(): void {
if ( ! function_exists( 'wc_price' ) ) {
return;
}
- $order_duplicates = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
+ $existing_order_duplicates = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
+ $dismissed_duplicates = get_option( self::DISMISSED_DUPLICATES_OPTION_NAME, [] );
?>
@@ -124,7 +142,11 @@ public static function display_admin_notice(): void {
add_meta_data( self::DISMISSED_DUPLICATE_ORDER_META_NAME, 1 );
- $wc_order->save();
- }
- }
- self::check_for_order_duplicates();
+ $dismissed_duplicates[] = $_POST['dismiss_order_ids']; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ update_option( self::DISMISSED_DUPLICATES_OPTION_NAME, $dismissed_duplicates );
// Refresh the page to reflect changes.
wp_safe_redirect( isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : admin_url() );
exit;
}
}
+
+ /**
+ * CLI handler to upsert the order duplicates with a specified timeframe.
+ *
+ * ## OPTIONS
+ *
+ * [--cutoff_time=]
+ * : The cutoff time in the past (e.g. "2 months").
+ *
+ * [--save]
+ * : Whether to save the results for display in the admin panel.
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack detect-order-duplicates --cutoff_time='2 months' --save
+ *
+ * @param array $args Positional args.
+ * @param array $assoc_args Associative args.
+ */
+ public static function cli_upsert_order_duplicates( $args, $assoc_args ) {
+ $cutoff_time_str = isset( $assoc_args['cutoff_time'] ) ? $assoc_args['cutoff_time'] : '1 month';
+ $cutoff_time = strtotime( $cutoff_time_str ) - time();
+ $save_as_option = isset( $assoc_args['save'] ) ? $assoc_args['save'] : false;
+
+ $duplicates = self::check_for_order_duplicates( $cutoff_time, $save_as_option, false );
+
+ if ( empty( $duplicates ) ) {
+ \WP_CLI::success( 'No duplicate orders found.' );
+ } else {
+ \WP_CLI::success( sprintf( '%d duplicate order series found.', count( $duplicates ) ) );
+ }
+ }
}
WooCommerce_Duplicate_Orders::init();
From 97bb2a1764feecf8ed0789ee17a5963b62775e2f Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Thu, 28 Nov 2024 12:53:51 +0100
Subject: [PATCH 09/10] feat: process orders in batches
---
.../class-woocommerce-duplicate-orders.php | 23 +++++++++++++++----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 2798f9b643..6bf39f835c 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -40,16 +40,26 @@ public static function init(): void {
* Duplicates will be detected if it the same amount, same day, from the same customer.
*
* @param number $cutoff_time The cutoff time in the past (how many seconds ago).
+ * @param number $current_page Current page of results.
+ * @param array $results Results to be merged with new results.
*/
- private static function get_order_duplicates( $cutoff_time ): array {
- $orders = wc_get_orders(
+ public static function get_order_duplicates( $cutoff_time, $current_page = 0, $results = [] ): array {
+ $per_page = 100;
+ $order_result = wc_get_orders(
[
- 'limit' => -1,
+ 'paginate' => true,
+ 'limit' => $per_page,
'status' => [ 'wc-completed' ],
+ 'offset' => $current_page * $per_page,
'date_completed' => '>' . ( time() - $cutoff_time ),
]
);
+ if ( defined( 'WP_CLI' ) && WP_CLI && $order_result->max_num_pages > 0 ) {
+ \WP_CLI::line( sprintf( 'Processing page %d/%d of orders.', $current_page + 1, $order_result->max_num_pages ) );
+ }
+
+ $orders = $order_result->orders;
$order_duplicates = [];
foreach ( $orders as $order ) {
@@ -76,8 +86,6 @@ private static function get_order_duplicates( $cutoff_time ): array {
$order_duplicates[ $email ][ $amount ][ $date ][] = $order->get_id();
}
- $results = [];
-
foreach ( $order_duplicates as $email => $amounts ) {
foreach ( $amounts as $amount => $dates ) {
foreach ( $dates as $date => $order_ids ) {
@@ -95,6 +103,11 @@ private static function get_order_duplicates( $cutoff_time ): array {
}
}
+ if ( $order_result->total > 0 ) {
+ $current_page++;
+ return self::get_order_duplicates( $cutoff_time, $current_page, $results );
+ }
+
return $results;
}
From 520f49c1367f00aebf272734c855943de2c7080a Mon Sep 17 00:00:00 2001
From: Adam Cassis
Date: Fri, 29 Nov 2024 09:03:49 +0100
Subject: [PATCH 10/10] feat: tweak
---
.../woocommerce/class-woocommerce-duplicate-orders.php | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
index 6bf39f835c..310747a469 100644
--- a/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
+++ b/includes/reader-revenue/woocommerce/class-woocommerce-duplicate-orders.php
@@ -118,7 +118,7 @@ public static function get_order_duplicates( $cutoff_time, $current_page = 0, $r
* @param bool $save Whether to save the result as the option.
* @param bool $upsert Whether to upsert the option (merge with existing).
*/
- public static function check_for_order_duplicates( $cutoff_time = MONTH_IN_SECONDS, $save = false, $upsert = true ): array {
+ public static function check_for_order_duplicates( $cutoff_time = DAY_IN_SECONDS, $save = false, $upsert = true ): array {
$order_duplicates = self::get_order_duplicates( $cutoff_time );
if ( empty( $order_duplicates ) ) {
return [];
@@ -146,6 +146,9 @@ public static function display_admin_notice(): void {
return;
}
$existing_order_duplicates = get_option( self::DUPLICATED_ORDERS_OPTION_NAME, [] );
+ if ( empty( $existing_order_duplicates ) ) {
+ return;
+ }
$dismissed_duplicates = get_option( self::DISMISSED_DUPLICATES_OPTION_NAME, [] );
?>
@@ -212,7 +215,7 @@ public static function display_admin_notice(): void {
}
/**
- * CLI handler to upsert the order duplicates with a specified timeframe.
+ * CLI handler to search for duplicates and optionally store this info to be displayed in the admin panel.
*
* ## OPTIONS
*