From ee119e0a7ed24cf8c83983ff013bd188e3081d19 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Tue, 21 Apr 2020 15:37:21 +0530 Subject: [PATCH] Revert "Introduce a `reserved stock` class and database table to prevent race conditions during checkout" --- includes/class-wc-cart.php | 3 +- includes/class-wc-checkout.php | 18 -- includes/class-wc-install.php | 9 - includes/class-woocommerce.php | 1 - .../class-wc-product-data-store-cpt.php | 25 +-- .../class-wc-shortcode-checkout.php | 7 +- includes/wc-stock-functions.php | 95 +++------- src/Checkout/Helpers/ReserveStock.php | 176 ------------------ .../Helpers/ReserveStockException.php | 62 ------ tests/unit-tests/checkout/checkout.php | 143 ++------------ .../order/class-wc-tests-orders.php | 2 +- 11 files changed, 48 insertions(+), 493 deletions(-) delete mode 100644 src/Checkout/Helpers/ReserveStock.php delete mode 100644 src/Checkout/Helpers/ReserveStockException.php diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index 4ca59ad0885..17378bfbf1b 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -765,6 +765,7 @@ 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 ) { @@ -783,7 +784,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 = wc_get_held_stock_quantity( $product, $current_session_order_id ); + $held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $current_session_order_id ) : 0; $required_stock = $product_qty_in_cart[ $product->get_stock_managed_by_id() ]; if ( $product->get_stock_quantity() < ( $held_stock + $required_stock ) ) { diff --git a/includes/class-wc-checkout.php b/includes/class-wc-checkout.php index 829ff63cee9..3a05d107527 100644 --- a/includes/class-wc-checkout.php +++ b/includes/class-wc-checkout.php @@ -388,30 +388,12 @@ 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() ); } diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 5bcfbe071d0..a219d7d8323 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -946,14 +946,6 @@ 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; "; @@ -988,7 +980,6 @@ CREATE TABLE {$wpdb->prefix}wc_reserved_stock ( "{$wpdb->prefix}woocommerce_shipping_zones", "{$wpdb->prefix}woocommerce_tax_rate_locations", "{$wpdb->prefix}woocommerce_tax_rates", - "{$wpdb->prefix}wc_reserved_stock", ); /** diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 48ecc9b01d2..f5acc2122c2 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -246,7 +246,6 @@ 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 ) { diff --git a/includes/data-stores/class-wc-product-data-store-cpt.php b/includes/data-stores/class-wc-product-data-store-cpt.php index e417c1d9c6a..6c2baf5afff 100644 --- a/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -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 ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $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. $searchand = ' AND '; } @@ -2062,23 +2062,4 @@ 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 - ); - } } diff --git a/includes/shortcodes/class-wc-shortcode-checkout.php b/includes/shortcodes/class-wc-shortcode-checkout.php index 31413213a72..6316d07e2f8 100644 --- a/includes/shortcodes/class-wc-shortcode-checkout.php +++ b/includes/shortcodes/class-wc-shortcode-checkout.php @@ -84,8 +84,9 @@ 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 ); + $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 or payment link is invalid. if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { @@ -157,7 +158,7 @@ class WC_Shortcode_Checkout { } // Check stock based on all items in the cart and consider any held stock within pending orders. - $held_stock = wc_get_held_stock_quantity( $product, $order->get_id() ); + $held_stock = ( $hold_stock_minutes > 0 ) ? wc_get_held_stock_quantity( $product, $order->get_id() ) : 0; $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 ) ) { diff --git a/includes/wc-stock-functions.php b/includes/wc-stock-functions.php index a153a33c32c..2deb1580e4c 100644 --- a/includes/wc-stock-functions.php +++ b/includes/wc-stock-functions.php @@ -299,80 +299,33 @@ function wc_increase_stock_levels( $order_id ) { * @param integer $exclude_order_id Order ID to exclude. * @return int */ -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; - } +function wc_get_held_stock_quantity( $product, $exclude_order_id = 0 ) { + global $wpdb; - return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); + 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. } -/** - * 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 * diff --git a/src/Checkout/Helpers/ReserveStock.php b/src/Checkout/Helpers/ReserveStock.php deleted file mode 100644 index b882cf83906..00000000000 --- a/src/Checkout/Helpers/ReserveStock.php +++ /dev/null @@ -1,176 +0,0 @@ -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 */ - __( '"%s" 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 - ); - } -} diff --git a/src/Checkout/Helpers/ReserveStockException.php b/src/Checkout/Helpers/ReserveStockException.php deleted file mode 100644 index e84409b5f1d..00000000000 --- a/src/Checkout/Helpers/ReserveStockException.php +++ /dev/null @@ -1,62 +0,0 @@ -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; - } -} diff --git a/tests/unit-tests/checkout/checkout.php b/tests/unit-tests/checkout/checkout.php index d6639ec88ab..fb2ad7ea82d 100644 --- a/tests/unit-tests/checkout/checkout.php +++ b/tests/unit-tests/checkout/checkout.php @@ -9,8 +9,9 @@ * Class WC_Checkout */ class WC_Tests_Checkout extends WC_Unit_Test_Case { + /** - * TearDown. + * TearDown for tests. */ public function tearDown() { parent::tearDown(); @@ -18,7 +19,7 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case { } /** - * Setup. + * Setup for tests. */ public function setUp() { parent::setUp(); @@ -31,24 +32,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() ); @@ -60,7 +61,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', ) ); @@ -74,8 +75,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( @@ -90,10 +91,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', ) ); @@ -109,7 +110,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', ) ); @@ -187,120 +188,4 @@ 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() ); - } } diff --git a/tests/unit-tests/order/class-wc-tests-orders.php b/tests/unit-tests/order/class-wc-tests-orders.php index 4877745a592..20305f95ed1 100644 --- a/tests/unit-tests/order/class-wc-tests-orders.php +++ b/tests/unit-tests/order/class-wc-tests-orders.php @@ -8,7 +8,7 @@ /** * Class WC_Tests_Order. */ -class WC_Tests_Orders extends WC_Unit_Test_Case { +class WC_Tests_Order extends WC_Unit_Test_Case { /** * Test for total when round at subtotal is enabled.