@@ -48,6 +48,22 @@ class WC_Payments_Remediate_Canceled_Auth_Fees {
4848 */
4949 const DRY_RUN_ACTION_HOOK = 'wcpay_remediate_canceled_authorization_fees_dry_run ' ;
5050
51+ /**
52+ * Action Scheduler hook for the async affected orders check.
53+ */
54+ const CHECK_AFFECTED_ORDERS_HOOK = 'wcpay_check_affected_auth_fee_orders ' ;
55+
56+ /**
57+ * Option key for tracking the affected orders check state.
58+ *
59+ * Possible values:
60+ * - false (option doesn't exist): not yet checked.
61+ * - 'scheduled': async check is scheduled or running.
62+ * - 'has_affected_orders': affected orders were found.
63+ * - 'no_affected_orders': no affected orders found.
64+ */
65+ const CHECK_STATE_OPTION_KEY = 'wcpay_has_affected_auth_fee_orders ' ;
66+
5167 /**
5268 * Option key for tracking dry run mode.
5369 */
@@ -98,6 +114,7 @@ public function __construct() {
98114 public function init (): void {
99115 add_action ( self ::ACTION_HOOK , [ $ this , 'process_batch ' ] );
100116 add_action ( self ::DRY_RUN_ACTION_HOOK , [ $ this , 'process_batch_dry_run ' ] );
117+ add_action ( self ::CHECK_AFFECTED_ORDERS_HOOK , [ $ this , 'check_and_cache_affected_orders ' ] );
101118 }
102119
103120 /**
@@ -294,35 +311,33 @@ private function get_affected_orders_hpos( int $limit ): array {
294311 $ orders_table = $ wpdb ->prefix . 'wc_orders ' ;
295312 $ meta_table = $ wpdb ->prefix . 'wc_orders_meta ' ;
296313
297- // Build the SQL query to find orders with canceled intent status that have either:
298- // 1. Incorrect fee metadata (_wcpay_transaction_fee or _wcpay_net), OR
299- // 2. Refund objects (which shouldn't exist for never-captured authorizations), OR
300- // 3. Incorrect order status of 'wc-refunded' (should be 'wc-cancelled').
301- $ sql = "
302- SELECT DISTINCT o.id
303- FROM {$ orders_table } o
304- INNER JOIN {$ meta_table } pm_status ON o.id = pm_status.order_id
305- LEFT JOIN {$ meta_table } pm_fee ON o.id = pm_fee.order_id
306- AND pm_fee.meta_key IN ('_wcpay_transaction_fee', '_wcpay_net')
307- LEFT JOIN {$ orders_table } refunds ON o.id = refunds.parent_order_id
308- AND refunds.type = 'shop_order_refund'
309- WHERE o.type = 'shop_order'
310- AND o.date_created_gmt >= %s
311- AND pm_status.meta_key = '_intention_status'
312- AND pm_status.meta_value = %s
313- AND (pm_fee.order_id IS NOT NULL OR refunds.id IS NOT NULL OR o.status = 'wc-refunded')
314- " ;
315-
316- $ params = [ self ::BUG_START_DATE , Intent_Status::CANCELED ];
314+ $ sql = "SELECT orders.id
315+ FROM {$ orders_table } orders
316+ INNER JOIN {$ meta_table } status_meta ON orders.id = status_meta.order_id AND status_meta.meta_key = '_intention_status' AND status_meta.meta_value = %s
317+ LEFT JOIN {$ meta_table } fees_meta ON orders.id = fees_meta.order_id AND fees_meta.meta_key = '_wcpay_transaction_fee'
318+ WHERE orders.type = 'shop_order'
319+ AND orders.date_created_gmt >= %s
320+ AND (
321+ -- Refunded with or without a refund.
322+ orders.status = 'wc-refunded'
323+
324+ -- Cancelled with fees.
325+ OR (
326+ orders.status = 'wc-cancelled'
327+ AND fees_meta.order_id IS NOT NULL
328+ )
329+ ) " ;
330+
331+ $ params = [ Intent_Status::CANCELED , self ::BUG_START_DATE ];
317332
318333 // Add offset based on last order ID.
319334 if ( $ last_order_id > 0 ) {
320- $ sql .= ' AND o .id > %d ' ;
335+ $ sql .= ' AND orders .id > %d ' ;
321336 $ params [] = $ last_order_id ;
322337 }
323338
324339 // Add ordering and limit.
325- $ sql .= ' ORDER BY o .id ASC LIMIT %d ' ;
340+ $ sql .= ' ORDER BY orders .id ASC LIMIT %d ' ;
326341 $ params [] = $ limit ;
327342
328343 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -346,31 +361,33 @@ private function get_affected_orders_cpt( int $limit ): array {
346361 // 1. Incorrect fee metadata (_wcpay_transaction_fee or _wcpay_net), OR
347362 // 2. Refund objects (which shouldn't exist for never-captured authorizations), OR
348363 // 3. Incorrect order status of 'wc-refunded' (should be 'wc-cancelled').
349- $ sql = "
350- SELECT DISTINCT p.ID
351- FROM {$ wpdb ->posts } p
352- INNER JOIN {$ wpdb ->postmeta } pm_status ON p.ID = pm_status.post_id
353- LEFT JOIN {$ wpdb ->postmeta } pm_fee ON p.ID = pm_fee.post_id
354- AND pm_fee.meta_key IN ('_wcpay_transaction_fee', '_wcpay_net')
355- LEFT JOIN {$ wpdb ->posts } refunds ON p.ID = refunds.post_parent
356- AND refunds.post_type = 'shop_order_refund'
357- WHERE p.post_type IN ('shop_order', 'shop_order_placeholder')
358- AND p.post_date >= %s
359- AND pm_status.meta_key = '_intention_status'
360- AND pm_status.meta_value = %s
361- AND (pm_fee.post_id IS NOT NULL OR refunds.ID IS NOT NULL OR p.post_status = 'wc-refunded')
362- " ;
363-
364- $ params = [ self ::BUG_START_DATE , Intent_Status::CANCELED ];
364+ $ sql = "SELECT orders.ID
365+ FROM {$ wpdb ->posts } orders
366+ INNER JOIN {$ wpdb ->postmeta } status_meta ON orders.ID = status_meta.post_id AND status_meta.meta_key = '_intention_status' AND status_meta.meta_value = %s
367+ LEFT JOIN {$ wpdb ->postmeta } fees_meta ON orders.ID = fees_meta.post_id AND fees_meta.meta_key = '_wcpay_transaction_fee'
368+ WHERE orders.post_type IN ('shop_order', 'shop_order_placeholder')
369+ AND orders.post_date >= %s
370+ AND (
371+ -- Refunded with or without a refund.
372+ orders.post_status = 'wc-refunded'
373+
374+ -- Cancelled with fees
375+ OR (
376+ orders.post_status = 'wc-cancelled'
377+ AND fees_meta.post_id IS NOT NULL
378+ )
379+ ) " ;
380+
381+ $ params = [ Intent_Status::CANCELED , self ::BUG_START_DATE ];
365382
366383 // Add offset based on last order ID.
367384 if ( $ last_order_id > 0 ) {
368- $ sql .= ' AND p .ID > %d ' ;
385+ $ sql .= ' AND orders .ID > %d ' ;
369386 $ params [] = $ last_order_id ;
370387 }
371388
372389 // Add ordering and limit.
373- $ sql .= ' ORDER BY p .ID ASC LIMIT %d ' ;
390+ $ sql .= ' ORDER BY orders .ID ASC LIMIT %d ' ;
374391 $ params [] = $ limit ;
375392
376393 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
@@ -427,6 +444,9 @@ public function process_batch(): void {
427444 return ;
428445 }
429446
447+ // This can affect the order transitions by unnecessarily reaching out to Stripe.
448+ remove_action ( 'woocommerce_order_status_cancelled ' , [ WC_Payments::get_order_service (), 'cancel_authorizations_on_order_status_change ' ] );
449+
430450 $ start_time = microtime ( true );
431451 $ batch_size = $ this ->get_batch_size ();
432452 $ orders = $ this ->get_affected_orders ( $ batch_size );
@@ -885,4 +905,21 @@ public function has_affected_orders(): bool {
885905 $ orders = $ this ->get_affected_orders ( 1 );
886906 return ! empty ( $ orders );
887907 }
908+
909+ /**
910+ * Run the affected orders query and cache the result.
911+ *
912+ * Called by Action Scheduler in a separate request.
913+ *
914+ * @return void
915+ */
916+ public function check_and_cache_affected_orders (): void {
917+ $ result = $ this ->has_affected_orders ();
918+
919+ update_option (
920+ self ::CHECK_STATE_OPTION_KEY ,
921+ $ result ? 'has_affected_orders ' : 'no_affected_orders ' ,
922+ true
923+ );
924+ }
888925}
0 commit comments