Move Draft order logic behind feature flag. (https://github.com/woocommerce/woocommerce-blocks/pull/2874)
* refactor all draft order functionality to be in it’s own class and feature gate it. * move and fix tests for draft order deletes * add test to ensure only draft orders are deleted * implement review feedback and assert valid results before deleting * update tests * doh method can’t be protected * fix conditional for removing scheduled action * switch to use Woo Core function for catching the exception * add tests for error handling. * use `$wpdb->prepare` and remove temp group on test
This commit is contained in:
parent
aee97ceb3f
commit
60b1422cf6
|
@ -22,6 +22,7 @@ use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque;
|
|||
use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal;
|
||||
use Automattic\WooCommerce\Blocks\Payments\Integrations\BankTransfer;
|
||||
use Automattic\WooCommerce\Blocks\Payments\Integrations\CashOnDelivery;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
|
||||
|
||||
/**
|
||||
* Takes care of bootstrapping the plugin.
|
||||
|
@ -79,7 +80,7 @@ class Bootstrap {
|
|||
$this->container->get( Installer::class );
|
||||
BlockAssets::init();
|
||||
}
|
||||
|
||||
$this->container->get( DraftOrders::class )->init();
|
||||
$this->container->get( PaymentsApi::class );
|
||||
$this->container->get( RestApi::class );
|
||||
Library::init();
|
||||
|
@ -192,6 +193,12 @@ class Bootstrap {
|
|||
return new Installer();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
DraftOrders::class,
|
||||
function( Container $container ) {
|
||||
return new DraftOrders( $container->get( Package::class ) );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
/**
|
||||
* Sets up all logic related to the Checkout Draft Orders service
|
||||
*
|
||||
* @package WooCommerce/Blocks
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Exception;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Service class for adding DraftOrder functionality to WooCommerce core.
|
||||
*/
|
||||
class DraftOrders {
|
||||
|
||||
const DB_STATUS = 'wc-checkout-draft';
|
||||
const STATUS = 'checkout-draft';
|
||||
|
||||
/**
|
||||
* Holds the Package instance
|
||||
*
|
||||
* @var Package
|
||||
*/
|
||||
private $package;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Package $package An instance of the package class.
|
||||
*/
|
||||
public function __construct( Package $package ) {
|
||||
$this->package = $package;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all hooks related to adding Checkout Draft order functionality to Woo Core.
|
||||
*/
|
||||
public function init() {
|
||||
if ( $this->package->is_feature_plugin_build() ) {
|
||||
add_filter( 'wc_order_statuses', [ $this, 'register_draft_order_status' ] );
|
||||
add_filter( 'woocommerce_register_shop_order_post_statuses', [ $this, 'register_draft_order_post_status' ] );
|
||||
add_filter( 'woocommerce_valid_order_statuses_for_payment', [ $this, 'append_draft_order_post_status' ] );
|
||||
add_action( 'woocommerce_cleanup_draft_orders', [ $this, 'delete_expired_draft_orders' ] );
|
||||
add_action( 'admin_init', [ $this, 'install' ] );
|
||||
} else {
|
||||
// Maybe remove existing cronjob if present because it shouldn't be needed in the environment.
|
||||
add_action( 'admin_init', [ $this, 'uninstall' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installation related logic for Draft order functionality.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function install() {
|
||||
$this->maybe_create_cronjobs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cronjobs if they exist (but only from admin).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function uninstall() {
|
||||
$this->maybe_remove_cronjobs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe create cron events.
|
||||
*/
|
||||
protected function maybe_create_cronjobs() {
|
||||
if ( function_exists( 'as_next_scheduled_action' ) && false === as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) {
|
||||
as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'woocommerce_cleanup_draft_orders' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule cron jobs that are present.
|
||||
*/
|
||||
protected function maybe_remove_cronjobs() {
|
||||
if ( function_exists( 'as_next_scheduled_action' ) && as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) {
|
||||
as_unschedule_all_actions( 'woocommerce_cleanup_draft_orders' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom order status for orders created via the API during checkout.
|
||||
*
|
||||
* Draft order status is used before payment is attempted, during checkout, when a cart is converted to an order.
|
||||
*
|
||||
* @param array $statuses Array of statuses.
|
||||
* @internal
|
||||
* @return array
|
||||
*/
|
||||
public function register_draft_order_status( array $statuses ) {
|
||||
$statuses[ self::DB_STATUS ] = _x( 'Draft', 'Order status', 'woo-gutenberg-products-block' );
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom order post status for orders created via the API during checkout.
|
||||
*
|
||||
* @param array $statuses Array of statuses.
|
||||
* @internal
|
||||
|
||||
* @return array
|
||||
*/
|
||||
public function register_draft_order_post_status( array $statuses ) {
|
||||
$statuses[ self::DB_STATUS ] = [
|
||||
'label' => _x( 'Draft', 'Order status', 'woo-gutenberg-products-block' ),
|
||||
'public' => false,
|
||||
'exclude_from_search' => false,
|
||||
'show_in_admin_all_list' => false,
|
||||
'show_in_admin_status_list' => true,
|
||||
/* translators: %s: number of orders */
|
||||
'label_count' => _n_noop( 'Drafts <span class="count">(%s)</span>', 'Drafts <span class="count">(%s)</span>', 'woo-gutenberg-products-block' ),
|
||||
];
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append draft status to a list of statuses.
|
||||
*
|
||||
* @param array $statuses Array of statuses.
|
||||
* @internal
|
||||
|
||||
* @return array
|
||||
*/
|
||||
public function append_draft_order_post_status( $statuses ) {
|
||||
$statuses[] = self::STATUS;
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete draft orders older than a day in batches of 20.
|
||||
*
|
||||
* Ran on a daily cron schedule.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function delete_expired_draft_orders() {
|
||||
$count = 0;
|
||||
$batch_size = 20;
|
||||
$orders = wc_get_orders(
|
||||
[
|
||||
'date_modified' => '<=' . strtotime( '-1 DAY' ),
|
||||
'limit' => $batch_size,
|
||||
'status' => self::DB_STATUS,
|
||||
'type' => 'shop_order',
|
||||
]
|
||||
);
|
||||
|
||||
// do we bail because the query results are unexpected?
|
||||
try {
|
||||
$this->assert_order_results( $orders, $batch_size );
|
||||
if ( $orders ) {
|
||||
foreach ( $orders as $order ) {
|
||||
$order->delete( true );
|
||||
$count ++;
|
||||
}
|
||||
}
|
||||
if ( $batch_size === $count && function_exists( 'as_enqueue_async_action' ) ) {
|
||||
as_enqueue_async_action( 'woocommerce_cleanup_draft_orders' );
|
||||
}
|
||||
} catch ( Exception $error ) {
|
||||
wc_caught_exception( $error, __METHOD__ );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts whether incoming order results are expected given the query
|
||||
* this service class executes.
|
||||
*
|
||||
* @param WC_Order[] $order_results The order results being asserted.
|
||||
* @param int $expected_batch_size The expected batch size for the results.
|
||||
* @throws Exception If any assertions fail, an exception is thrown.
|
||||
*/
|
||||
private function assert_order_results( $order_results, $expected_batch_size ) {
|
||||
// if not an array, then just return because it won't get handled
|
||||
// anyways.
|
||||
if ( ! is_array( $order_results ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$suffix = ' This is an indicator that something is filtering WooCommerce or WordPress queries and modifying the query parameters.';
|
||||
|
||||
// if count is greater than our expected batch size, then that's a problem.
|
||||
if ( count( $order_results ) > 20 ) {
|
||||
throw new Exception( 'There are an unexpected number of results returned from the query.' . $suffix );
|
||||
}
|
||||
|
||||
// if any of the returned orders are not draft (or not a WC_Order), then that's a problem.
|
||||
foreach ( $order_results as $order ) {
|
||||
if ( ! ( $order instanceof WC_Order ) ) {
|
||||
throw new Exception( 'The returned results contain a value that is not a WC_Order.' . $suffix );
|
||||
}
|
||||
if ( ! $order->has_status( self::STATUS ) ) {
|
||||
throw new Exception( 'The results contain an order that is not a `wc-checkout-draft` status in the results.' . $suffix );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,6 @@ class Installer {
|
|||
*/
|
||||
public function install() {
|
||||
$this->maybe_create_tables();
|
||||
$this->maybe_create_cronjobs();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,13 +119,4 @@ class Installer {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe create cron events.
|
||||
*/
|
||||
protected function maybe_create_cronjobs() {
|
||||
if ( function_exists( 'as_next_scheduled_action' ) && false === as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) {
|
||||
as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'woocommerce_cleanup_draft_orders' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,6 @@ class Library {
|
|||
public static function init() {
|
||||
add_action( 'init', array( __CLASS__, 'register_blocks' ) );
|
||||
add_action( 'init', array( __CLASS__, 'define_tables' ) );
|
||||
add_filter( 'wc_order_statuses', array( __CLASS__, 'register_draft_order_status' ) );
|
||||
add_filter( 'woocommerce_register_shop_order_post_statuses', array( __CLASS__, 'register_draft_order_post_status' ) );
|
||||
add_filter( 'woocommerce_valid_order_statuses_for_payment', array( __CLASS__, 'append_draft_order_post_status' ) );
|
||||
add_action( 'woocommerce_cleanup_draft_orders', array( __CLASS__, 'delete_expired_draft_orders' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,76 +110,4 @@ class Library {
|
|||
$instance->register_block_type();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom order status for orders created via the API during checkout.
|
||||
*
|
||||
* Draft order status is used before payment is attempted, during checkout, when a cart is converted to an order.
|
||||
*
|
||||
* @param array $statuses Array of statuses.
|
||||
* @return array
|
||||
*/
|
||||
public static function register_draft_order_status( array $statuses ) {
|
||||
$statuses['wc-checkout-draft'] = _x( 'Draft', 'Order status', 'woo-gutenberg-products-block' );
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom order post status for orders created via the API during checkout.
|
||||
*
|
||||
* @param array $statuses Array of statuses.
|
||||
* @return array
|
||||
*/
|
||||
public static function register_draft_order_post_status( array $statuses ) {
|
||||
$statuses['wc-checkout-draft'] = [
|
||||
'label' => _x( 'Draft', 'Order status', 'woo-gutenberg-products-block' ),
|
||||
'public' => false,
|
||||
'exclude_from_search' => false,
|
||||
'show_in_admin_all_list' => false,
|
||||
'show_in_admin_status_list' => true,
|
||||
/* translators: %s: number of orders */
|
||||
'label_count' => _n_noop( 'Drafts <span class="count">(%s)</span>', 'Drafts <span class="count">(%s)</span>', 'woo-gutenberg-products-block' ),
|
||||
];
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append draft status to a list of statuses.
|
||||
*
|
||||
* @param array $statuses Array of statuses.
|
||||
* @return array
|
||||
*/
|
||||
public static function append_draft_order_post_status( $statuses ) {
|
||||
$statuses[] = 'checkout-draft';
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete draft orders older than a day in batches of 20.
|
||||
*
|
||||
* Ran on a daily cron schedule.
|
||||
*/
|
||||
public static function delete_expired_draft_orders() {
|
||||
$count = 0;
|
||||
$batch_size = 20;
|
||||
$orders = wc_get_orders(
|
||||
[
|
||||
'date_modified' => '<=' . strtotime( '-1 DAY' ),
|
||||
'limit' => $batch_size,
|
||||
'status' => 'wc-checkout-draft',
|
||||
'type' => 'shop_order',
|
||||
]
|
||||
);
|
||||
|
||||
if ( $orders ) {
|
||||
foreach ( $orders as $order ) {
|
||||
$order->delete( true );
|
||||
$count ++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $batch_size === $count && function_exists( 'as_enqueue_async_action' ) ) {
|
||||
as_enqueue_async_action( 'woocommerce_cleanup_draft_orders' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Tests\Library;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use \WC_Order;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
|
||||
/**
|
||||
* Tests Delete Draft Orders functionality
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
class DeleteDraftOrders extends TestCase {
|
||||
|
||||
private $draft_orders_instance;
|
||||
private $caught_exception;
|
||||
|
||||
/**
|
||||
* During setup create some draft orders.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUp() {
|
||||
global $wpdb;
|
||||
|
||||
$this->draft_orders_instance = new DraftOrders( new Package( 'test', './') );
|
||||
|
||||
$order = new WC_Order();
|
||||
$order->set_status( DraftOrders::STATUS );
|
||||
$order->save();
|
||||
|
||||
$order = new WC_Order();
|
||||
$order->set_status( DraftOrders::STATUS );
|
||||
$order->save();
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_modified' => date( 'Y-m-d H:i:s', strtotime( '-1 DAY', current_time( 'timestamp' ) ) ),
|
||||
'post_modified_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 DAY' ) )
|
||||
),
|
||||
array(
|
||||
'ID' => $order->get_id()
|
||||
)
|
||||
);
|
||||
|
||||
$order = new WC_Order();
|
||||
$order->set_status( DraftOrders::STATUS );
|
||||
$order->save();
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_modified' => date( 'Y-m-d H:i:s', strtotime( '-2 DAY', current_time( 'timestamp' ) ) ),
|
||||
'post_modified_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 DAY' ) )
|
||||
),
|
||||
array(
|
||||
'ID' => $order->get_id()
|
||||
)
|
||||
);
|
||||
|
||||
// set a non-draft order to make sure it's unaffected
|
||||
$order = new WC_Order();
|
||||
$order->set_status( 'on-hold' );
|
||||
$order->save();
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_modified' => date( 'Y-m-d H:i:s', strtotime( '-2 DAY', current_time( 'timestamp' ) ) ),
|
||||
'post_modified_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 DAY' ) )
|
||||
),
|
||||
array(
|
||||
'ID' => $order->get_id()
|
||||
)
|
||||
);
|
||||
|
||||
// set listening for exceptions
|
||||
add_action( 'woocommerce_caught_exception', function($exception_object){
|
||||
$this->caught_exception = $exception_object;
|
||||
});
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
$this->draft_orders_instance = null;
|
||||
remove_all_actions( 'woocommerce_caught_exception' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete draft orders older than a day.
|
||||
*
|
||||
* Ran on a daily cron schedule.
|
||||
*/
|
||||
public function test_delete_expired_draft_orders() {
|
||||
global $wpdb;
|
||||
$status = DraftOrders::DB_STATUS;
|
||||
|
||||
// Check there are 3 draft orders from our setup before running tests.
|
||||
$this->assertEquals( 3, (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) from $wpdb->posts posts WHERE posts.post_status = '%s'", [ $status ] ) ) );
|
||||
|
||||
// Run delete query.
|
||||
$this->draft_orders_instance->delete_expired_draft_orders();
|
||||
|
||||
// Only 1 should remain.
|
||||
$this->assertEquals( 1, (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) from $wpdb->posts posts WHERE posts.post_status = '%s'", [ $status ] ) ) );
|
||||
|
||||
// The non-draft order should still be present
|
||||
$this->assertEquals( 1, (int) $wpdb->get_var( "SELECT COUNT(ID) from $wpdb->posts posts WHERE posts.post_status = 'wc-on-hold'" ) );
|
||||
}
|
||||
|
||||
public function test_greater_than_batch_results_error() {
|
||||
$sample_results = function() {
|
||||
return array_fill( 0, 21, ( new WC_Order ) );
|
||||
|
||||
};
|
||||
$this->mock_results_for_wc_query($sample_results);
|
||||
$this->draft_orders_instance->delete_expired_draft_orders();
|
||||
$this->assertContains( 'unexpected number of results', $this->caught_exception->getMessage() );
|
||||
$this->unset_mock_results_for_wc_query( $sample_results );
|
||||
}
|
||||
|
||||
public function test_order_not_instance_of_wc_order_error() {
|
||||
$sample_results = function() {
|
||||
return [ 10 ];
|
||||
};
|
||||
$this->mock_results_for_wc_query( $sample_results );
|
||||
$this->draft_orders_instance->delete_expired_draft_orders();
|
||||
$this->assertContains( 'value that is not a WC_Order', $this->caught_exception->getMessage() );
|
||||
$this->unset_mock_results_for_wc_query( $sample_results );
|
||||
}
|
||||
|
||||
public function test_order_incorrect_status_error() {
|
||||
$sample_results = function() {
|
||||
$test_order = new WC_Order();
|
||||
$test_order->set_status( 'on-hold' );
|
||||
return [ $test_order ];
|
||||
};
|
||||
$this->mock_results_for_wc_query( $sample_results );
|
||||
$this->draft_orders_instance->delete_expired_draft_orders();
|
||||
$this->assertContains( 'order that is not a `wc-checkout-draft`', $this->caught_exception->getMessage() );
|
||||
$this->unset_mock_results_for_wc_query( $sample_results );
|
||||
}
|
||||
|
||||
private function mock_results_for_wc_query( $mock_callback ) {
|
||||
add_filter( 'woocommerce_order_query', $mock_callback );
|
||||
}
|
||||
|
||||
private function unset_mock_results_for_wc_query( $mock_callback ) {
|
||||
remove_filter( 'woocommerce_order_query', $mock_callback );
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Tests\Library;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use \WC_Order;
|
||||
use Automattic\WooCommerce\Blocks\Library;
|
||||
|
||||
/**
|
||||
* Tests Delete Draft Orders functionality
|
||||
*
|
||||
* @since $VID:$
|
||||
* @group testing
|
||||
*/
|
||||
class DeleteDraftOrders extends TestCase {
|
||||
/**
|
||||
* During setup create some draft orders.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUp() {
|
||||
global $wpdb;
|
||||
|
||||
$order = new WC_Order();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->save();
|
||||
|
||||
$order = new WC_Order();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->save();
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_modified' => date( 'Y-m-d H:i:s', strtotime( '-1 DAY', current_time( 'timestamp' ) ) ),
|
||||
'post_modified_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 DAY' ) )
|
||||
),
|
||||
array(
|
||||
'ID' => $order->get_id()
|
||||
)
|
||||
);
|
||||
|
||||
$order = new WC_Order();
|
||||
$order->set_status( 'checkout-draft' );
|
||||
$order->save();
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_modified' => date( 'Y-m-d H:i:s', strtotime( '-2 DAY', current_time( 'timestamp' ) ) ),
|
||||
'post_modified_gmt' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 DAY' ) )
|
||||
),
|
||||
array(
|
||||
'ID' => $order->get_id()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete draft orders older than a day.
|
||||
*
|
||||
* Ran on a daily cron schedule.
|
||||
*/
|
||||
public function test_delete_expired_draft_orders() {
|
||||
global $wpdb;
|
||||
|
||||
// Check there are 3 draft orders from our setup before running tests.
|
||||
$this->assertEquals( 3, (int) $wpdb->get_var( "SELECT COUNT(ID) from $wpdb->posts posts WHERE posts.post_status = 'wc-checkout-draft'" ) );
|
||||
|
||||
// Run delete query.
|
||||
Library::delete_expired_draft_orders();
|
||||
|
||||
// Only 1 should remain.
|
||||
$this->assertEquals( 1, (int) $wpdb->get_var( "SELECT COUNT(ID) from $wpdb->posts posts WHERE posts.post_status = 'wc-checkout-draft'" ) );
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue