Skip to content

Commit 5ddded3

Browse files
mgascamclaudeRadoslavGeorgiev
committed
fix: cache affected orders check for canceled auth fee remediation note (#11334)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Radoslav Georgiev <rageorgiev@gmail.com>
1 parent cbed641 commit 5ddded3

5 files changed

+214
-50
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fix
3+
4+
Cache the affected orders check for the canceled auth fee remediation note to avoid an expensive query on every admin page load

includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

includes/notes/class-wc-payments-notes-canceled-auth-remediation.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class WC_Payments_Notes_Canceled_Auth_Remediation {
3737
* @return bool
3838
*/
3939
public static function can_be_added() {
40+
include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
41+
4042
// Don't show if remediation is already complete.
4143
if ( 'completed' === get_option( 'wcpay_fee_remediation_status', '' ) ) {
4244
return false;
@@ -88,12 +90,47 @@ public static function get_note() {
8890
/**
8991
* Check if there are orders that need remediation.
9092
*
93+
* Uses a state machine backed by an option to avoid running the expensive
94+
* query inline. On the first call, schedules an async Action Scheduler job
95+
* and returns false. The note will be added on a subsequent admin_init
96+
* once the async check completes.
97+
*
9198
* @return bool
9299
*/
93100
private static function has_affected_orders() {
94-
include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
95-
$remediation = new WC_Payments_Remediate_Canceled_Auth_Fees();
96-
return $remediation->has_affected_orders();
101+
$state = get_option( WC_Payments_Remediate_Canceled_Auth_Fees::CHECK_STATE_OPTION_KEY );
102+
103+
if ( false === $state ) {
104+
self::schedule_check();
105+
return false;
106+
}
107+
108+
if ( 'has_affected_orders' === $state ) {
109+
return true;
110+
}
111+
112+
// 'scheduled', 'no_affected_orders', or any unexpected value.
113+
return false;
114+
}
115+
116+
/**
117+
* Schedule the async affected orders check via Action Scheduler.
118+
*
119+
* @return void
120+
*/
121+
private static function schedule_check() {
122+
if ( ! function_exists( 'as_schedule_single_action' ) ) {
123+
return;
124+
}
125+
126+
update_option( WC_Payments_Remediate_Canceled_Auth_Fees::CHECK_STATE_OPTION_KEY, 'scheduled', true );
127+
128+
as_schedule_single_action(
129+
time() + 10,
130+
WC_Payments_Remediate_Canceled_Auth_Fees::CHECK_AFFECTED_ORDERS_HOOK,
131+
[],
132+
'woocommerce-payments'
133+
);
97134
}
98135

99136
/**
@@ -106,7 +143,6 @@ private static function is_remediation_running() {
106143
return false;
107144
}
108145

109-
include_once WCPAY_ABSPATH . 'includes/migrations/class-wc-payments-remediate-canceled-auth-fees.php';
110146
return as_has_scheduled_action( WC_Payments_Remediate_Canceled_Auth_Fees::ACTION_HOOK );
111147
}
112148
}

tests/unit/migrations/test-class-wc-payments-remediate-canceled-auth-fees.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ public function test_increment_stat_updates_counter() {
102102
}
103103

104104
public function test_get_affected_orders_returns_canceled_orders_with_fees() {
105-
// Create order with canceled intent and fees.
105+
// Create order with canceled intent, cancelled status, and fees.
106106
$order = WC_Helper_Order::create_order();
107107
$order->set_date_created( '2023-05-01' );
108+
$order->set_status( 'cancelled' );
108109
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
109110
$order->update_meta_data( '_wcpay_transaction_fee', '1.50' );
110111
$order->save();
@@ -153,10 +154,12 @@ public function test_get_affected_orders_excludes_orders_without_fees_or_refunds
153154
$this->assertCount( 0, $orders );
154155
}
155156

156-
public function test_get_affected_orders_finds_orders_with_refunds_but_no_fees() {
157-
// Create order with canceled intent and refund, but no fee metadata.
157+
public function test_get_affected_orders_excludes_cancelled_orders_with_refunds_but_no_fees() {
158+
// Create cancelled order with canceled intent and refund, but no fee metadata.
159+
// The new query only matches cancelled orders when they have fee metadata.
158160
$order = WC_Helper_Order::create_order();
159161
$order->set_date_created( '2023-05-01' );
162+
$order->set_status( 'cancelled' );
160163
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
161164
$order->save();
162165

@@ -171,15 +174,15 @@ public function test_get_affected_orders_finds_orders_with_refunds_but_no_fees()
171174

172175
$orders = $this->remediation->get_affected_orders( 10 );
173176

174-
$this->assertCount( 1, $orders );
175-
$this->assertEquals( $order->get_id(), $orders[0]->get_id() );
177+
$this->assertCount( 0, $orders );
176178
}
177179

178180
public function test_get_affected_orders_respects_batch_size() {
179181
// Create 5 affected orders.
180182
for ( $i = 0; $i < 5; $i++ ) {
181183
$order = WC_Helper_Order::create_order();
182184
$order->set_date_created( '2023-05-01' );
185+
$order->set_status( 'cancelled' );
183186
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
184187
$order->update_meta_data( '_wcpay_transaction_fee', '1.50' );
185188
$order->save();
@@ -194,18 +197,21 @@ public function test_get_affected_orders_uses_offset_from_last_order_id() {
194197
// Create 3 affected orders.
195198
$order1 = WC_Helper_Order::create_order();
196199
$order1->set_date_created( '2023-05-01' );
200+
$order1->set_status( 'cancelled' );
197201
$order1->update_meta_data( '_intention_status', Intent_Status::CANCELED );
198202
$order1->update_meta_data( '_wcpay_transaction_fee', '1.50' );
199203
$order1->save();
200204

201205
$order2 = WC_Helper_Order::create_order();
202206
$order2->set_date_created( '2023-05-02' );
207+
$order2->set_status( 'cancelled' );
203208
$order2->update_meta_data( '_intention_status', Intent_Status::CANCELED );
204209
$order2->update_meta_data( '_wcpay_transaction_fee', '1.50' );
205210
$order2->save();
206211

207212
$order3 = WC_Helper_Order::create_order();
208213
$order3->set_date_created( '2023-05-03' );
214+
$order3->set_status( 'cancelled' );
209215
$order3->update_meta_data( '_intention_status', Intent_Status::CANCELED );
210216
$order3->update_meta_data( '_wcpay_transaction_fee', '1.50' );
211217
$order3->save();
@@ -465,6 +471,7 @@ public function test_process_batch_remediates_affected_orders() {
465471
for ( $i = 0; $i < 3; $i++ ) {
466472
$order = WC_Helper_Order::create_order();
467473
$order->set_date_created( '2023-05-01' );
474+
$order->set_status( 'cancelled' );
468475
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
469476
$order->update_meta_data( '_wcpay_transaction_fee', '1.50' );
470477
$order->save();
@@ -481,12 +488,14 @@ public function test_process_batch_remediates_affected_orders() {
481488
public function test_process_batch_updates_last_order_id() {
482489
$order1 = WC_Helper_Order::create_order();
483490
$order1->set_date_created( '2023-05-01' );
491+
$order1->set_status( 'cancelled' );
484492
$order1->update_meta_data( '_intention_status', Intent_Status::CANCELED );
485493
$order1->update_meta_data( '_wcpay_transaction_fee', '1.50' );
486494
$order1->save();
487495

488496
$order2 = WC_Helper_Order::create_order();
489497
$order2->set_date_created( '2023-05-02' );
498+
$order2->set_status( 'cancelled' );
490499
$order2->update_meta_data( '_intention_status', Intent_Status::CANCELED );
491500
$order2->update_meta_data( '_wcpay_transaction_fee', '1.50' );
492501
$order2->save();
@@ -508,6 +517,7 @@ public function test_process_batch_marks_complete_when_no_orders() {
508517
public function test_process_batch_increments_error_count_on_failure() {
509518
$order = WC_Helper_Order::create_order();
510519
$order->set_date_created( '2023-05-01' );
520+
$order->set_status( 'cancelled' );
511521
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
512522
$order->update_meta_data( '_wcpay_transaction_fee', '1.50' );
513523
$order->save();
@@ -540,6 +550,7 @@ public function test_has_affected_orders_returns_true_when_orders_exist() {
540550
// Create an affected order.
541551
$order = WC_Helper_Order::create_order();
542552
$order->set_date_created( '2023-05-01' );
553+
$order->set_status( 'cancelled' );
543554
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
544555
$order->update_meta_data( '_wcpay_transaction_fee', '1.50' );
545556
$order->save();
@@ -584,9 +595,10 @@ public function test_get_affected_orders_uses_cpt_when_hpos_disabled() {
584595

585596
$mock_remediation->method( 'is_hpos_enabled' )->willReturn( false );
586597

587-
// Create order with canceled intent and fees.
598+
// Create order with canceled intent, cancelled status, and fees.
588599
$order = WC_Helper_Order::create_order();
589600
$order->set_date_created( '2023-05-01' );
601+
$order->set_status( 'cancelled' );
590602
$order->update_meta_data( '_intention_status', Intent_Status::CANCELED );
591603
$order->update_meta_data( '_wcpay_transaction_fee', '1.50' );
592604
$order->save();

0 commit comments

Comments
 (0)