diff --git a/includes/class-wc-checkout.php b/includes/class-wc-checkout.php index 5207ae41519..1b477db2cf9 100644 --- a/includes/class-wc-checkout.php +++ b/includes/class-wc-checkout.php @@ -366,7 +366,6 @@ class WC_Checkout { } } - $order->hold_stock_for_checkout( WC()->cart ); $order->set_created_via( 'checkout' ); $order->set_cart_hash( $cart_hash ); $order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) ); @@ -404,9 +403,6 @@ class WC_Checkout { return $order_id; } catch ( Exception $e ) { - if ( $order && $order instanceof WC_Order ) { - $order->release_held_stock(); - } return new WP_Error( 'checkout-error', $e->getMessage() ); } } diff --git a/includes/class-wc-order.php b/includes/class-wc-order.php index 08bfcd2eb73..520b2a7fdb0 100644 --- a/includes/class-wc-order.php +++ b/includes/class-wc-order.php @@ -2059,135 +2059,4 @@ class WC_Order extends WC_Abstract_Order { return apply_filters( 'woocommerce_get_order_item_totals', $total_rows, $this, $tax_display ); } - - /** - * Adds a '_held_for_checkout` record for all products in cart. - * - * @since 3.9.0 - * @param WC_Cart $cart Cart instance. - * @throws Exception When unable to hold stock for checkout. - */ - public function hold_stock_for_checkout( $cart ) { - /** - * Filter: woocommerce_hold_stock_for_checkout - * Allows enable/disable hold stock functionality on checkout. - * - * @since 3.9.0 - * @param bool $enabled Default to true. - */ - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { - return; - } - - if ( ! get_option( 'woocommerce_manage_stock' ) ) { - return; - } - - $hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 0 ); - if ( 0 >= $hold_stock_minutes ) { - return; - } - - $product_qty_in_cart = $cart->get_cart_item_quantities(); - $stock_held_keys = array(); - $error = null; - - try { - foreach ( $cart->get_cart() as $cart_item_key => $values ) { - $product = wc_get_product( $values['data'] ); - if ( ! $product ) { - // Unsupported product! - continue; - } - $product_id = $product->get_stock_managed_by_id(); - $result = $this->hold_product_for_checkout( $product, $product_qty_in_cart[ $product_id ] ); - if ( false === $result ) { - // translators: Name of the product. - throw new Exception( sprintf( __( 'Something changed during checkout. %s is no longer available.', 'woocommerce' ), $product->get_name() ) ); - } - if ( null === $result ) { - continue; - } - $stock_held_keys[ $product_id ] = $result; - } - } catch ( Exception $e ) { - $error = $e; - } finally { - if ( 0 < count( $stock_held_keys ) ) { - $this->record_held_stock( $stock_held_keys ); - } - if ( $error instanceof Exception ) { - throw $error; - } - } - } - - /** - * Adds a `_held_for_checkout` record for a product in checkout. - * - * @since 3.9.0 - * @param WC_Product $product Instance of product. - * @param int $quantity Quantity of product to hold. - * @return bool|string|null Returns `false` when unable to hold stock, meta key when stock was held successfully, `null` when holding stock is not needed. - */ - protected function hold_product_for_checkout( $product, $quantity ) { - global $wpdb; - if ( ! $product->managing_stock() || $product->backorders_allowed() ) { - return null; - } - $product_id = $product->get_stock_managed_by_id(); - $product_data_store = WC_Data_Store::load( 'product' ); - $held_unique_str = wp_generate_password( 6, false ); - $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); - $hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 60 ); - $expire_timestamp = (int) $db_timestamp + ( $hold_stock_minutes * 60 ); - $held_key = "_held_for_checkout_${expire_timestamp}_$held_unique_str"; - $query_for_held_stock = $product_data_store->get_query_for_held_stock( $product_id ); - $query_for_stock = $product_data_store->get_query_for_stock( $product_id ); - - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $insert_statement = $wpdb->prepare( - " - INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) - SELECT %d, %s, %d from DUAL - WHERE ( $query_for_stock ) - ( $query_for_held_stock ) >= %d - ", - $product->get_stock_managed_by_id(), - $held_key, - $quantity, - $quantity - ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared - - $result = $wpdb->query( $insert_statement ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - - return $result > 0 ? $held_key : false; - } - - /** - * Save keys used to held stock to DB. - * - * @since 3.9.0 - * @param array $keys Array of keys to save. - */ - public function record_held_stock( $keys ) { - if ( is_array( $keys ) && 0 < count( $keys ) ) { - $this->update_meta_data( '_stock_held_keys', $keys ); - } - } - - /** - * Releases held stock, also deletes keys for the order. - * - * @since 3.9.0 - */ - public function release_held_stock() { - $stock_held_keys = $this->get_meta( '_stock_held_keys' ); - if ( is_array( $stock_held_keys ) ) { - foreach ( $stock_held_keys as $product_managed_id => $meta_key ) { - delete_post_meta( $product_managed_id, $meta_key ); - } - } - $this->delete_meta_data( '_stock_held_keys' ); - } } 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 c51b4748d7c..b855cdf97c9 100644 --- a/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -879,8 +879,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.NotPrepared " SELECT posts.ID as id, posts.post_parent as parent_id FROM {$wpdb->posts} AS posts @@ -898,8 +898,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.NotPrepared ); - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** @@ -1595,7 +1595,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 '; } @@ -2052,48 +2052,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. - * - * @since 3.9.0 - * @param int $product_id Product ID. - * @return string|void Query statement. - */ - public function get_query_for_stock( $product_id ) { - global $wpdb; - return $wpdb->prepare( - // MAX function below is used to make sure result is a scalar. - " - SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta - WHERE {$wpdb->postmeta}.meta_key = '_stock' - AND {$wpdb->postmeta}.post_id = %d - FOR UPDATE - ", - $product_id - ); - } - - /** - * Returns query statement for getting quantity of stock held by orders in checkout. - * - * @since 3.9.0 - * @param int $product_id Product ID. - * @return string|void Query statement. - */ - public function get_query_for_held_stock( $product_id ) { - global $wpdb; - return $wpdb->prepare( - " - SELECT COALESCE ( SUM( meta_value ), 0 ) FROM $wpdb->postmeta - WHERE {$wpdb->postmeta}.meta_key like %s - AND {$wpdb->postmeta}.meta_key > CONCAT( %s, UNIX_TIMESTAMP() ) - AND {$wpdb->postmeta}.post_id = %d - FOR UPDATE - ", - '_held_for_checkout_%', - '_held_for_checkout_', - $product_id - ); - } } diff --git a/includes/shortcodes/class-wc-shortcode-checkout.php b/includes/shortcodes/class-wc-shortcode-checkout.php index a107376d7cd..4b9265a56a2 100644 --- a/includes/shortcodes/class-wc-shortcode-checkout.php +++ b/includes/shortcodes/class-wc-shortcode-checkout.php @@ -150,7 +150,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 ) : 0; + $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 ( $product->get_stock_quantity() < ( $held_stock + $required_stock ) ) { diff --git a/includes/wc-order-functions.php b/includes/wc-order-functions.php index 2623b5ed01a..33238294a7a 100644 --- a/includes/wc-order-functions.php +++ b/includes/wc-order-functions.php @@ -803,8 +803,6 @@ function wc_order_search( $term ) { function wc_update_total_sales_counts( $order_id ) { $order = wc_get_order( $order_id ); - $order->release_held_stock(); - if ( ! $order || $order->get_data_store()->get_recorded_sales( $order ) ) { return; } @@ -882,23 +880,6 @@ function wc_update_coupon_usage_counts( $order_id ) { } } } - -/** - * Release held stock if any for an order. - * - * @since 3.9.0 - * @param int $order_id Order ID. - */ -function wc_release_held_stock( $order_id ) { - $order = wc_get_order( $order_id ); - if ( ! $order ) { - return; - } - - $order->release_held_stock(); -} -add_action( 'woocommerce_order_status_cancelled', 'wc_release_held_stock' ); - add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' ); add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); diff --git a/includes/wc-stock-functions.php b/includes/wc-stock-functions.php index 7e1ae5b6809..2fb6d112338 100644 --- a/includes/wc-stock-functions.php +++ b/includes/wc-stock-functions.php @@ -302,53 +302,28 @@ function wc_increase_stock_levels( $order_id ) { function wc_get_held_stock_quantity( $product, $exclude_order_id = 0 ) { global $wpdb; - /** - * Filter: woocommerce_hold_stock_for_checkout - * Allows enable/disable hold stock functionality on checkout. - * - * @since 3.9.0 - * @param bool $enabled Default to true. - */ - if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { - /** - * DOES NOT SUPPORT `exclude_order_id` param, which was primarily being used to exclude the current order! - * While creating an order during checkout flow, this logic relies on the fact that order is not yet saved and therefore will be excluded. - * - * Query for calculating held stock which uses '_held_for_checkout' records instead of actually querying all orders with wc-pending status. - * This is introduced to handle race conditions. - * - * @since 3.9.0 - */ - $product_data_store = WC_Data_Store::load( 'product' ); - $product_id = $product->get_stock_managed_by_id(); - return $wpdb->get_var( $product_data_store->get_query_for_held_stock( $product_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - } - return $wpdb->get_var( - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $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; - ", + 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 ) - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared - ); + ); // WPCS: unprepared SQL ok. } /** diff --git a/tests/unit-tests/checkout/checkout.php b/tests/unit-tests/checkout/checkout.php deleted file mode 100644 index 5ed48cd9e6f..00000000000 --- a/tests/unit-tests/checkout/checkout.php +++ /dev/null @@ -1,151 +0,0 @@ -cart->empty_cart(); - } - - /** - * Setup. - */ - public function setUp() { - parent::setUp(); - WC()->cart->empty_cart(); - } - - /** - * Helper method to create a managed product and a order for that product. - * - * @return array( WC_Product, WC_Order ) 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() ); - } - - /** - * Legacy version for test `test_create_order_when_out_of_stock` above. - * @throws Exception When unable to create order. - */ - public function test_create_order_when_out_of_stock_legacy() { - add_filter( 'woocommerce_hold_stock_for_checkout', '__return_false' ); - $this->test_create_order_when_out_of_stock(); - } - - /** - * 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-order.php b/tests/unit-tests/order/class-wc-tests-order.php deleted file mode 100644 index efbee3333b9..00000000000 --- a/tests/unit-tests/order/class-wc-tests-order.php +++ /dev/null @@ -1,93 +0,0 @@ -cart->empty_cart(); - } - - /** - * Test pending stock and `release_held_stock` and `record_held_stock` methods as well. - * @throws Exception When unable to create an order. - */ - public function test_pending_stock_for_order_with_multiple_product() { - $product1 = WC_Helper_Product::create_simple_product(); - $product1->set_props( array( 'manage_stock' => true ) ); - $product1->set_stock_quantity( 10 ); - $product1->save(); - - $product2 = WC_Helper_Product::create_simple_product(); - $product2->set_props( array( 'manage_stock' => true ) ); - $product2->set_stock_quantity( 10 ); - $product2->save(); - - WC()->cart->add_to_cart( $product1->get_id(), 9 ); - WC()->cart->add_to_cart( $product2->get_id(), 5 ); - $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', - ) - ); - - $this->assertNotWPError( $order_id ); - $order_held_stock_keys = get_post_meta( $order_id, '_stock_held_keys', true ); - - $product1_id = $product1->get_stock_managed_by_id(); - $product2_id = $product2->get_stock_managed_by_id(); - - $this->assertEquals( true, in_array( $product1_id, array_keys( $order_held_stock_keys ) ) ); - $this->assertEquals( true, in_array( $product2_id, array_keys( $order_held_stock_keys ) ) ); - - $this->assertEquals( 9, wc_get_held_stock_quantity( $product1 ) ); - $this->assertEquals( 5, wc_get_held_stock_quantity( $product2 ) ); - - $order = wc_get_order( $order_id ); - $order->release_held_stock(); - - $this->assertEquals( 0, wc_get_held_stock_quantity( $product1 ) ); - $this->assertEquals( 0, wc_get_held_stock_quantity( $product2 ) ); - } - - /** - * Test `release_held_stock` function of unmanaged product. - * @throws Exception When unable to create an order. - */ - public function test_release_held_of_unmanaged_product() { - $product = WC_Helper_Product::create_simple_product(); - - 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', - ) - ); - - $this->assertNotWPError( $order_id ); - $order = wc_get_order( $order_id ); - $this->assertEquals( 0, wc_get_held_stock_quantity( $product ) ); - - $order->release_held_stock(); - $this->assertEquals( 0, wc_get_held_stock_quantity( $product ) ); - } -}