* 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:
Darren Ethier 2020-07-16 12:19:31 -04:00 committed by GitHub
parent aee97ceb3f
commit 60b1422cf6
6 changed files with 364 additions and 161 deletions

View File

@ -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 ) );
}
);
}
/**

View File

@ -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 );
}
}
}
}

View File

@ -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' );
}
}
}

View File

@ -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' );
}
}
}

View File

@ -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 );
}
}

View File

@ -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'" ) );
}
}