Fix/Fix new order triggers when creating/moving orders into draft or from draft to valid statuses (#49098)

* Prevent new order hook if moving or creating order status into a non-triggering status (drafts&failed refunded etc) when using CPT for orders.

* Removed rogue error_log()

* Lint

* Prevent new order hook if moving or creating order status into a non-triggering status (drafts&failed refunded etc) when using HPOS for orders.

* Added changelog

* Fixed wrong if simplification

* Unit tests for update methods

* Renamed var

* Renamed vars to snake_case.

* Introduced remove_status_prefix() utility function.

* Added default 'new' status to an Order that doesn't exist yet in the database for the update method in HPOS

* Added default 'new' status to an Order that doesn't exist yet in the database for the update method in CPT

* Linting.

* Missing class import.

* Added a new test for updating new processing Orders.

* Linting.

* Tests fix.

* Tests fix.

* Removed statuses from triggering the new order hook.

* tweak

* Test tweak

* Test tweak

* Test tweak

* Test tweak

* Test tweak

* Tweak to rule out db querying failure.
This commit is contained in:
Paulo Arromba 2024-07-23 16:08:53 +01:00 committed by GitHub
parent 3d148e2577
commit 1ba24e1937
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 192 additions and 16 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Resolved issues with new order hook triggers during transitions to and from draft statuses.

View File

@ -626,7 +626,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
*/
public function set_status( $new_status ) {
$old_status = $this->get_status();
$new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status;
$new_status = OrderUtil::remove_status_prefix( $new_status );
$status_exceptions = array( 'auto-draft', 'trash' );

View File

@ -5,6 +5,8 @@
* @package WooCommerce\Classes
*/
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@ -189,22 +191,37 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
// Also grab the current status so we can compare.
$previous_status = get_post_status( $order->get_id() );
// If the order doesn't exist in the DB, we will consider it as new.
if ( ! $previous_status && $order->get_id() === 0 ) {
$previous_status = 'new';
}
// Update the order.
parent::update( $order );
// Fire a hook depending on the status - this should be considered a creation if it was previously draft status.
$new_status = $order->get_status( 'edit' );
$current_status = $order->get_status( 'edit' );
if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft', 'checkout-draft' ), true ) ) {
do_action( 'woocommerce_new_order', $order->get_id(), $order );
} else {
do_action( 'woocommerce_update_order', $order->get_id(), $order );
// We need to remove the wc- prefix from the status for comparison and proper evaluation of new vs updated orders.
$previous_status = OrderUtil::remove_status_prefix( $previous_status );
$current_status = OrderUtil::remove_status_prefix( $current_status );
$draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' );
// This hook should be fired only if the new status is not one of draft statuses and the previous status was one of the draft statuses.
if (
$current_status !== $previous_status
&& ! in_array( $current_status, $draft_statuses, true )
&& in_array( $previous_status, $draft_statuses, true )
) {
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return;
}
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
* Helper method that updates all the post meta for an order based on it's settings in the WC_Order class.
* Helper method that updates all the post meta for an order based on its settings in the WC_Order class.
*
* @param WC_Order $order Order object.
* @since 3.0.0

View File

@ -468,7 +468,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
}
// Format the order status.
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
$data['status'] = OrderUtil::remove_status_prefix( $data['status'] );
// Format line items.
foreach ( $format_line_items as $key ) {

View File

@ -10,6 +10,7 @@
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\Utilities\Users;
use Automattic\WooCommerce\Utilities\OrderUtil;
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
@ -147,9 +148,9 @@ function wc_get_is_pending_statuses() {
*/
function wc_get_order_status_name( $status ) {
$statuses = wc_get_order_statuses();
$status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
$status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status;
return $status;
$status = OrderUtil::remove_status_prefix( $status );
return $statuses[ 'wc-' . $status ] ?? $status;
}
/**

View File

@ -239,7 +239,7 @@ class Orders extends \WC_REST_Orders_Controller {
}
// Format the order status.
$data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status'];
$data['status'] = OrderUtil::remove_status_prefix( $data['status'] );
// Format requested line items.
$formatted_line_items = array();

View File

@ -2586,7 +2586,7 @@ FROM $order_meta_table
* @param \WC_Order $order Order object.
*/
public function update( &$order ) {
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' );
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status', 'new' );
// Before updating, ensure date paid is set if missing.
if (
@ -2621,8 +2621,15 @@ FROM $order_meta_table
$order->apply_changes();
$this->clear_caches( $order );
// For backwards compatibility, moving a draft order to a valid status triggers the 'woocommerce_new_order' hook.
if ( ! empty( $changes['status'] ) && in_array( $previous_status, array( 'new', 'auto-draft', 'draft', 'checkout-draft' ), true ) ) {
$draft_statuses = array( 'new', 'auto-draft', 'draft', 'checkout-draft' );
// For backwards compatibility, this hook should be fired only if the new status is not one of the draft statuses and the previous status was one of the draft statuses.
if (
! empty( $changes['status'] )
&& $changes['status'] !== $previous_status
&& ! in_array( $changes['status'], $draft_statuses, true )
&& in_array( $previous_status, $draft_statuses, true )
) {
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return;
}

View File

@ -228,4 +228,19 @@ final class OrderUtil {
return $count_per_status;
}
/**
* Removes the 'wc-' prefix from status.
*
* @param string $status The status to remove the prefix from.
*
* @return string The status without the prefix.
* @since 9.2.0
*/
public static function remove_status_prefix( string $status ): string {
if ( strpos( $status, 'wc-' ) === 0 ) {
$status = substr( $status, 3 );
}
return $status;
}
}

View File

@ -466,4 +466,74 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
remove_action( 'woocommerce_new_order', $callback );
}
/**
* @testDox Updating an order status correctly triggers the "woocommerce_new_order" action.
*/
public function test_update_order_status_correctly_triggers_new_order_hook() {
$new_count = 0;
$callback = function () use ( &$new_count ) {
++$new_count;
};
add_action( 'woocommerce_new_order', $callback );
$order_data_store_cpt = new WC_Order_Data_Store_CPT();
$order = new WC_Order();
$order->set_status( 'draft' );
$this->assertEquals( 0, $new_count );
$order->set_status( 'checkout-draft' );
$order_data_store_cpt->update( $order );
$order->save();
$this->assertEquals( 0, $new_count );
$triggering_order_statuses = array( 'pending', 'on-hold', 'completed', 'processing' );
foreach ( $triggering_order_statuses as $k => $status ) {
$current_status = $order->get_status( 'edit' );
$order->set_status( $status );
$order_data_store_cpt->update( $order );
$order->set_status( 'checkout-draft' ); // Revert back to draft.
$order->save();
$this->assertEquals(
$k + 1,
$new_count,
'Failed to trigger new order hook changing status: ' . $current_status . ' -> ' . $status
);
}
remove_action( 'woocommerce_new_order', $callback );
}
/**
* @testDox Create a new order with processing status without saving and updating it should trigger the "woocommerce_new_order" action.
*/
public function test_update_new_processing_order_correctly_triggers_new_order_hook() {
$new_count = 0;
$callback = function () use ( &$new_count ) {
++$new_count;
};
add_action( 'woocommerce_new_order', $callback );
$order_data_store_cpt = new WC_Order_Data_Store_CPT();
$order = new WC_Order();
$order->set_status( 'processing' );
$this->assertEquals( 0, $new_count );
$order_data_store_cpt->update( $order );
$this->assertEquals( 1, $new_count );
remove_action( 'woocommerce_new_order', $callback );
}
}

View File

@ -3461,4 +3461,66 @@ class OrdersTableDataStoreTests extends HposTestCase {
remove_action( 'woocommerce_new_order', $callback );
}
/**
* @testDox Updating an order status correctly triggers the "woocommerce_new_order" action.
*/
public function test_update_order_status_correctly_triggers_new_order_hook() {
$new_count = 0;
$callback = function () use ( &$new_count ) {
++$new_count;
};
add_action( 'woocommerce_new_order', $callback );
$order = new WC_Order();
$order->set_status( 'draft' );
$this->assertEquals( 0, $new_count );
$order->set_status( 'checkout-draft' );
$this->sut->update( $order );
$order->save();
$this->assertEquals( 0, $new_count );
$triggering_order_statuses = array( 'pending', 'on-hold', 'completed', 'processing' );
foreach ( $triggering_order_statuses as $status ) {
$order->set_status( $status );
$this->sut->update( $order );
$order->set_status( 'checkout-draft' ); // Revert back to draft.
$order->save();
}
$this->assertEquals( 4, $new_count );
remove_action( 'woocommerce_new_order', $callback );
}
/**
* @testDox Create a new order with processing status without saving and updating it should trigger the "woocommerce_new_order" action.
*/
public function test_update_new_processing_order_correctly_triggers_new_order_hook() {
$new_count = 0;
$callback = function () use ( &$new_count ) {
++$new_count;
};
add_action( 'woocommerce_new_order', $callback );
$order = new WC_Order();
$order->set_status( 'processing' );
$this->assertEquals( 0, $new_count );
$this->sut->update( $order );
$order->save();
$this->assertEquals( 1, $new_count );
remove_action( 'woocommerce_new_order', $callback );
}
}