Revert "Revert "Introduce a `reserved stock` class and database table to prevent race conditions during checkout""

This commit is contained in:
Vedanshu Jain 2020-05-06 21:44:53 +05:30 committed by vedanshujain
parent 6564847802
commit b45373fe0c
11 changed files with 493 additions and 48 deletions

View File

@ -765,7 +765,6 @@ class WC_Cart extends WC_Legacy_Cart {
public function check_cart_item_stock() {
$error = new WP_Error();
$product_qty_in_cart = $this->get_cart_item_quantities();
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
$current_session_order_id = isset( WC()->session->order_awaiting_payment ) ? absint( WC()->session->order_awaiting_payment ) : 0;
foreach ( $this->get_cart() as $cart_item_key => $values ) {
@ -784,7 +783,7 @@ class WC_Cart extends WC_Legacy_Cart {
}
// Check stock based on all items in the cart and consider any held stock within pending orders.
$held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $current_session_order_id ) : 0;
$held_stock = wc_get_held_stock_quantity( $product, $current_session_order_id );
$required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ];
if ( $product->get_stock_quantity() < ( $held_stock + $required_stock ) ) {

View File

@ -388,12 +388,30 @@ class WC_Checkout {
// Save the order.
$order_id = $order->save();
/**
* Action hook fired after an order is created used to add custom meta to the order.
*
* @since 3.0.0
*/
do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data );
/**
* Action hook fired after an order is created.
*
* @since 4.1.0
*/
do_action( 'woocommerce_checkout_order_created', $order );
return $order_id;
} catch ( Exception $e ) {
if ( $order && $order instanceof WC_Order ) {
$order->get_data_store()->release_held_coupons( $order );
/**
* Action hook fired when an order is discarded due to Exception.
*
* @since 4.1.0
*/
do_action( 'woocommerce_checkout_order_exception', $order );
}
return new WP_Error( 'checkout-error', $e->getMessage() );
}

View File

@ -989,6 +989,14 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
slug varchar(200) NOT NULL DEFAULT '',
PRIMARY KEY (tax_rate_class_id),
UNIQUE KEY slug (slug($max_index_length))
) $collate;
CREATE TABLE {$wpdb->prefix}wc_reserved_stock (
`order_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`stock_quantity` double NOT NULL DEFAULT 0,
`timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`expires` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`, `product_id`)
) $collate;
";
@ -1023,6 +1031,7 @@ CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes (
"{$wpdb->prefix}woocommerce_shipping_zones",
"{$wpdb->prefix}woocommerce_tax_rate_locations",
"{$wpdb->prefix}woocommerce_tax_rates",
"{$wpdb->prefix}wc_reserved_stock",
);
/**

View File

@ -255,6 +255,7 @@ final class WooCommerce {
'order_itemmeta' => 'woocommerce_order_itemmeta',
'wc_product_meta_lookup' => 'wc_product_meta_lookup',
'wc_tax_rate_classes' => 'wc_tax_rate_classes',
'wc_reserved_stock' => 'wc_reserved_stock',
);
foreach ( $tables as $name => $table ) {

View File

@ -881,8 +881,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$outofstock_where = ' AND exclude_join.object_id IS NULL';
}
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return $wpdb->get_results(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"
SELECT posts.ID as id, posts.post_parent as parent_id
FROM {$wpdb->posts} AS posts
@ -900,8 +900,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
)
GROUP BY posts.ID
"
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
}
/**
@ -1603,7 +1603,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
foreach ( $search_terms as $search_term ) {
$like = '%' . $wpdb->esc_like( $search_term ) . '%';
$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) )", $like, $like, $like, $like ); // @codingStandardsIgnoreLine.
$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) )", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$searchand = ' AND ';
}
@ -2062,4 +2062,23 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
}
return '';
}
/**
* Returns query statement for getting current `_stock` of a product.
*
* @internal MAX function below is used to make sure result is a scalar.
* @param int $product_id Product ID.
* @return string|void Query statement.
*/
public function get_query_for_stock( $product_id ) {
global $wpdb;
return $wpdb->prepare(
"
SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table
WHERE meta_table.meta_key = '_stock'
AND meta_table.post_id = %d
",
$product_id
);
}
}

View File

@ -84,9 +84,8 @@ class WC_Shortcode_Checkout {
// Pay for existing order.
if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // WPCS: input var ok, CSRF ok.
try {
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
$order = wc_get_order( $order_id );
$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 );
$order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok.
$order = wc_get_order( $order_id );
// Order or payment link is invalid.
if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
@ -158,7 +157,7 @@ class WC_Shortcode_Checkout {
}
// Check stock based on all items in the cart and consider any held stock within pending orders.
$held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $order->get_id() ) : 0;
$held_stock = wc_get_held_stock_quantity( $product, $order->get_id() );
$required_stock = $quantities[ $product->get_stock_managed_by_id() ];
if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {

View File

@ -299,33 +299,80 @@ function wc_increase_stock_levels( $order_id ) {
* @param integer $exclude_order_id Order ID to exclude.
* @return int
*/
function wc_get_held_stock_quantity( $product, $exclude_order_id = 0 ) {
global $wpdb;
function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) {
/**
* Filter: woocommerce_hold_stock_for_checkout
* Allows enable/disable hold stock functionality on checkout.
*
* @since 4.1.0
* @param bool $enabled Default to true if managing stock globally.
*/
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
return 0;
}
return $wpdb->get_var(
$wpdb->prepare(
"
SELECT SUM( order_item_meta.meta_value ) AS held_qty
FROM {$wpdb->posts} AS posts
LEFT JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id
LEFT JOIN {$wpdb->prefix}woocommerce_order_items as order_items ON posts.ID = order_items.order_id
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id
LEFT JOIN {$wpdb->prefix}woocommerce_order_itemmeta as order_item_meta2 ON order_items.order_item_id = order_item_meta2.order_item_id
WHERE order_item_meta.meta_key = '_qty'
AND order_item_meta2.meta_key = %s
AND order_item_meta2.meta_value = %d
AND postmeta.meta_key = '_created_via'
AND postmeta.meta_value = 'checkout'
AND posts.post_type IN ( '" . implode( "','", wc_get_order_types() ) . "' )
AND posts.post_status = 'wc-pending'
AND posts.ID != %d;",
'product_variation' === get_post_type( $product->get_stock_managed_by_id() ) ? '_variation_id' : '_product_id',
$product->get_stock_managed_by_id(),
$exclude_order_id
)
); // WPCS: unprepared SQL ok.
return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id );
}
/**
* Hold stock for an order.
*
* @throws ReserveStockException If reserve stock fails.
*
* @since 4.1.0
* @param \WC_Order|int $order Order ID or instance.
*/
function wc_reserve_stock_for_order( $order ) {
/**
* Filter: woocommerce_hold_stock_for_checkout
* Allows enable/disable hold stock functionality on checkout.
*
* @since @since 4.1.0
* @param bool $enabled Default to true if managing stock globally.
*/
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
return;
}
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' );
/**
* Release held stock for an order.
*
* @since 4.1.0
* @param \WC_Order|int $order Order ID or instance.
*/
function wc_release_stock_for_order( $order ) {
/**
* Filter: woocommerce_hold_stock_for_checkout
* Allows enable/disable hold stock functionality on checkout.
*
* @since 4.1.0
* @param bool $enabled Default to true if managing stock globally.
*/
if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) {
return;
}
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' );
add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 );
add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 );
/**
* Return low stock amount to determine if notification needs to be sent
*

View File

@ -0,0 +1,176 @@
<?php
/**
* Handle product stock reservation during checkout.
*
* @package Automattic/WooCommerce
*/
namespace Automattic\WooCommerce\Checkout\Helpers;
defined( 'ABSPATH' ) || exit;
/**
* Stock Reservation class.
*/
final class ReserveStock {
/**
* Query for any existing holds on stock for this item.
*
* @param \WC_Product $product Product to get reserved stock for.
* @param integer $exclude_order_id Optional order to exclude from the results.
*
* @return integer Amount of stock already reserved.
*/
public function get_reserved_stock( \WC_Product $product, $exclude_order_id = 0 ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
return (int) $wpdb->get_var( $this->get_query_for_reserved_stock( $product->get_stock_managed_by_id(), $exclude_order_id ) );
}
/**
* Put a temporary hold on stock for an order if enough is available.
*
* @throws ReserveStockException If stock cannot be reserved.
*
* @param \WC_Order $order Order object.
* @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
*/
public function reserve_stock_for_order( \WC_Order $order, $minutes = 0 ) {
$minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 );
if ( ! $minutes ) {
return;
}
try {
$items = array_filter(
$order->get_items(),
function( $item ) {
return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0;
}
);
$rows = array();
foreach ( $items as $item ) {
$product = $item->get_product();
if ( ! $product->is_in_stock() ) {
throw new ReserveStockException(
'woocommerce_product_out_of_stock',
sprintf(
/* translators: %s: product name */
__( '&quot;%s&quot; is out of stock and cannot be purchased.', 'woocommerce' ),
$product->get_name()
),
403
);
}
// If stock management is off, no need to reserve any stock here.
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
continue;
}
$managed_by_id = $product->get_stock_managed_by_id();
$rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item->get_quantity() : $item->get_quantity();
}
if ( ! empty( $rows ) ) {
foreach ( $rows as $product_id => $quantity ) {
$this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes );
}
}
} catch ( ReserveStockException $e ) {
$this->release_stock_for_order( $order );
throw $e;
}
}
/**
* Release a temporary hold on stock for an order.
*
* @param \WC_Order $order Order object.
*/
public function release_stock_for_order( \WC_Order $order ) {
global $wpdb;
$wpdb->delete(
$wpdb->wc_reserved_stock,
array(
'order_id' => $order->get_id(),
)
);
}
/**
* Reserve stock for a product by inserting rows into the DB.
*
* @throws ReserveStockException If a row cannot be inserted.
*
* @param int $product_id Product ID which is having stock reserved.
* @param int $stock_quantity Stock amount to reserve.
* @param \WC_Order $order Order object which contains the product.
* @param int $minutes How long to reserve stock in minutes.
*/
private function reserve_stock_for_product( $product_id, $stock_quantity, \WC_Order $order, $minutes ) {
global $wpdb;
$product_data_store = \WC_Data_Store::load( 'product' );
$query_for_stock = $product_data_store->get_query_for_stock( $product_id );
$query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() );
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->query(
$wpdb->prepare(
"
REPLACE INTO {$wpdb->wc_reserved_stock} ( order_id, product_id, stock_quantity, expires )
SELECT %d, %d, %d, ( NOW() + INTERVAL %d MINUTE ) from DUAL
WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d
",
$order->get_id(),
$product_id,
$stock_quantity,
$minutes,
$stock_quantity
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
if ( ! $result ) {
$product = wc_get_product( $product_id );
throw new ReserveStockException(
'woocommerce_product_not_enough_stock',
sprintf(
/* translators: %s: product name */
__( 'Not enough units of %s are available in stock to fulfil this order.', 'woocommerce' ),
$product ? $product->get_name() : '#' . $product_id
),
403
);
}
}
/**
* Returns query statement for getting reserved stock of a product.
*
* @param int $product_id Product ID.
* @param integer $exclude_order_id Optional order to exclude from the results.
* @return string|void Query statement.
*/
private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) {
global $wpdb;
return $wpdb->prepare(
"
SELECT COALESCE( SUM( stock_table.`stock_quantity` ), 0 ) FROM $wpdb->wc_reserved_stock stock_table
LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID
WHERE posts.post_status IN ( 'wc-checkout-draft', 'wc-pending' )
AND stock_table.`expires` > NOW()
AND stock_table.`product_id` = %d
AND stock_table.`order_id` != %d
",
$product_id,
$exclude_order_id
);
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Exceptions for stock reservation.
*
* @package Automattic/WooCommerce
*/
namespace Automattic\WooCommerce\Checkout\Helpers;
defined( 'ABSPATH' ) || exit;
/**
* ReserveStockException class.
*/
class ReserveStockException extends \Exception {
/**
* Sanitized error code.
*
* @var string
*/
protected $error_code;
/**
* Error extra data.
*
* @var array
*/
protected $error_data;
/**
* Setup exception.
*
* @param string $code Machine-readable error code, e.g `woocommerce_invalid_product_id`.
* @param string $message User-friendly translated error message, e.g. 'Product ID is invalid'.
* @param int $http_status_code Proper HTTP status code to respond with, e.g. 400.
* @param array $data Extra error data.
*/
public function __construct( $code, $message, $http_status_code = 400, $data = array() ) {
$this->error_code = $code;
$this->error_data = $data;
parent::__construct( $message, $http_status_code );
}
/**
* Returns the error code.
*
* @return string
*/
public function getErrorCode() {
return $this->error_code;
}
/**
* Returns error data.
*
* @return array
*/
public function getErrorData() {
return $this->error_data;
}
}

View File

@ -9,9 +9,8 @@
* Class WC_Checkout
*/
class WC_Tests_Checkout extends WC_Unit_Test_Case {
/**
* TearDown for tests.
* TearDown.
*/
public function tearDown() {
parent::tearDown();
@ -19,7 +18,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
}
/**
* Setup for tests.
* Setup.
*/
public function setUp() {
parent::setUp();
@ -32,24 +31,24 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
* @throws Exception When unable to create order.
*/
public function test_create_order_with_limited_coupon() {
$coupon_code = 'coupon4one';
$coupon_code = 'coupon4one';
$coupon_data_store = WC_Data_Store::load( 'coupon' );
$coupon = WC_Helper_Coupon::create_coupon(
$coupon = WC_Helper_Coupon::create_coupon(
$coupon_code,
array( 'usage_limit' => 1 )
);
$product = WC_Helper_Product::create_simple_product( true );
$product = WC_Helper_Product::create_simple_product( true );
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon->get_code() );
$checkout = WC_Checkout::instance();
$order_id = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
'billing_email' => 'a@b.com',
'payment_method' => 'dummy_payment_gateway',
)
);
$this->assertNotWPError( $order_id );
$order = new WC_Order( $order_id );
$order = new WC_Order( $order_id );
$coupon_held_key = $order->get_data_store()->get_coupon_held_keys( $order );
$this->assertEquals( count( $coupon_held_key ), 1 );
$this->assertEquals( array_keys( $coupon_held_key )[0], $coupon->get_id() );
@ -61,7 +60,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
WC()->cart->add_discount( $coupon->get_code() );
$order2_id = $checkout->create_order(
array(
'billing_email' => 'a@c.com',
'billing_email' => 'a@c.com',
'payment_method' => 'dummy_payment_gateway',
)
);
@ -75,8 +74,8 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
* @throws Exception When unable to create an order.
*/
public function test_create_order_with_multiple_limited_coupons() {
$coupon_code1 = 'coupon1';
$coupon_code2 = 'coupon2';
$coupon_code1 = 'coupon1';
$coupon_code2 = 'coupon2';
$coupon_data_store = WC_Data_Store::load( 'coupon' );
$coupon1 = WC_Helper_Coupon::create_coupon(
@ -91,10 +90,10 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon_code1 );
WC()->cart->add_discount( $coupon_code2 );
$checkout = WC_Checkout::instance();
$checkout = WC_Checkout::instance();
$order_id1 = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
'billing_email' => 'a@b.com',
'payment_method' => 'dummy_payment_gateway',
)
);
@ -110,7 +109,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$order2_id = $checkout->create_order(
array(
'billing_email' => 'a@b.com',
'billing_email' => 'a@b.com',
'payment_method' => 'dummy_payment_gateway',
)
);
@ -188,4 +187,120 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
return 0.01;
}
/**
* Helper method to create a managed product and a order for that product.
*
* @return array
* @throws Exception When unable to create an order .
*/
protected function create_order_for_managed_inventory_product() {
$product = WC_Helper_Product::create_simple_product();
$product->set_props( array( 'manage_stock' => true ) );
$product->set_stock_quantity( 10 );
$product->save();
WC()->cart->add_to_cart( $product->get_id(), 9 );
$this->assertEquals( true, WC()->cart->check_cart_items() );
$checkout = WC_Checkout::instance();
$order_id = $checkout->create_order(
array(
'payment_method' => 'cod',
'billing_email' => 'a@b.com',
)
);
// Assertions whether the order was created successfully.
$this->assertNotWPError( $order_id );
$order = wc_get_order( $order_id );
return array( $product, $order );
}
/**
* Test when order is out stock because it is held by an order in pending status.
*
* @throws Exception When unable to create order.
*/
public function test_create_order_when_out_of_stock() {
list( $product, $order ) = $this->create_order_for_managed_inventory_product();
$this->assertEquals( 9, $order->get_item_count() );
$this->assertEquals( 'pending', $order->get_status() );
$this->assertEquals( 9, wc_get_held_stock_quantity( $product ) );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_stock_managed_by_id(), 2 );
$this->assertEquals( false, WC()->cart->check_cart_items() );
}
/**
* Test if pending stock is cleared when order is cancelled.
*
* @throws Exception When unable to create order.
*/
public function test_pending_is_cleared_when_order_is_cancelled() {
list( $product, $order ) = $this->create_order_for_managed_inventory_product();
$this->assertEquals( 9, wc_get_held_stock_quantity( $product ) );
$order->set_status( 'cancelled' );
$order->save();
$this->assertEquals( 0, wc_get_held_stock_quantity( $product ) );
$this->assertEquals( 10, $product->get_stock_quantity() );
}
/**
* Test if pending stock is cleared when order is processing.
*
* @throws Exception When unable to create order.
*/
public function test_pending_is_cleared_when_order_processed() {
list( $product, $order ) = $this->create_order_for_managed_inventory_product();
$this->assertEquals( 9, wc_get_held_stock_quantity( $product ) );
$order->set_status( 'processing' );
$order->save();
$this->assertEquals( 0, wc_get_held_stock_quantity( $product ) );
}
/**
* Test creating order from managed stock for variable product.
*
* @throws Exception When unable to create an order.
*/
public function test_create_order_for_variation_product() {
$parent_product = WC_Helper_Product::create_variation_product();
$variation = $parent_product->get_available_variations()[0];
$variation = wc_get_product( $variation['variation_id'] );
$variation->set_manage_stock( true );
$variation->set_stock_quantity( 10 );
$variation->save();
WC()->cart->add_to_cart( $variation->get_id(), 9 );
$this->assertEquals( true, WC()->cart->check_cart_items() );
$checkout = WC_Checkout::instance();
$order_id = $checkout->create_order(
array(
'payment_method' => 'cod',
'billing_email' => 'a@b.com',
)
);
// Assertions whether the first order was created successfully.
$this->assertNotWPError( $order_id );
$order = wc_get_order( $order_id );
$this->assertEquals( 9, $order->get_item_count() );
$this->assertEquals( 'pending', $order->get_status() );
$this->assertEquals( 9, wc_get_held_stock_quantity( $variation ) );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $variation->get_stock_managed_by_id(), 2 );
$this->assertEquals( false, WC()->cart->check_cart_items() );
}
}

View File

@ -8,7 +8,7 @@
/**
* Class WC_Tests_Order.
*/
class WC_Tests_Order extends WC_Unit_Test_Case {
class WC_Tests_Orders extends WC_Unit_Test_Case {
/**
* Test for total when round at subtotal is enabled.