Avoid reserving stock for draft orders under payment is attempted (#49446)

* Use wc_reserve_stock_for_order only before taking payment

* Release stock on exception

* changelog
This commit is contained in:
Mike Jolley 2024-07-16 12:00:23 +01:00 committed by GitHub
parent f3e003c236
commit 6d52388b8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 38 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Updated block checkout and Store API stock handling so stock is only reserved when attempting payment for an order.

View File

@ -10,6 +10,8 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
/**
* Update a product's stock amount.
*
@ -348,7 +350,8 @@ function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0
return 0;
}
return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id );
$reserve_stock = new ReserveStock();
return $reserve_stock->get_reserved_stock( $product, $exclude_order_id );
}
/**
@ -374,7 +377,8 @@ function wc_reserve_stock_for_order( $order ) {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order );
$reserve_stock = new ReserveStock();
$reserve_stock->reserve_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' );
@ -400,7 +404,8 @@ function wc_release_stock_for_order( $order ) {
$order = $order instanceof WC_Order ? $order : wc_get_order( $order );
if ( $order ) {
( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order );
$reserve_stock = new ReserveStock();
$reserve_stock->release_stock_for_order( $order );
}
}
add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' );

View File

@ -2,11 +2,9 @@
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
use Automattic\WooCommerce\Utilities\RestApiUtil;
@ -147,6 +145,11 @@ class Checkout extends AbstractCartRoute {
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
// If we encountered an exception, free up stock.
if ( $this->order ) {
wc_release_stock_for_order( $this->order );
}
}
return $this->add_response_headers( $response );
@ -181,7 +184,6 @@ class Checkout extends AbstractCartRoute {
* 5. Process Payment
*
* @throws RouteException On error.
* @throws InvalidStockLevelsInCartException On error.
*
* @param \WP_REST_Request $request Request object.
*
@ -195,35 +197,56 @@ class Checkout extends AbstractCartRoute {
$this->cart_controller->calculate_totals();
/**
* Validate items etc are allowed in the order before the order is processed. This will fix violations and tell
* the customer.
* Validate items and fix violations before the order is processed.
*/
$this->cart_controller->validate_cart();
/**
* Obtain Draft Order and process request data.
*
* Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart
* Persist customer session data from the request first so that OrderController::update_addresses_from_cart
* uses the up to date customer address.
*/
$this->update_customer_from_request( $request );
$this->create_or_update_draft_order( $request );
$this->update_order_from_request( $request );
/**
* Process customer data.
*
* Update order with customer details, and sign up a user account as necessary.
* Create (or update) Draft Order and process request data.
*/
$this->create_or_update_draft_order( $request );
$this->update_order_from_request( $request );
$this->process_customer( $request );
/**
* Validate order.
*
* This logic ensures the order is valid before payment is attempted.
* Validate updated order before payment is attempted.
*/
$this->order_controller->validate_order_before_payment( $this->order );
/**
* Reserve stock for the order.
*
* In the shortcode based checkout, when POSTing the checkout form the order would be created and fire the
* `woocommerce_checkout_order_created` action. This in turn would trigger the `wc_reserve_stock_for_order`
* function so that stock would be held pending payment.
*
* Via the block based checkout and Store API we already have a draft order, but when POSTing to the /checkout
* endpoint we do the same; reserve stock for the order to allow time to process payment.
*
* Note, stock is only "held" while the order has the status wc-checkout-draft or pending. Stock is freed when
* the order changes status, or there is an exception.
*
* @see ReserveStock::get_query_for_reserved_stock()
*
* @since 9.2 Stock is no longer held for all draft orders, nor on non-POST requests. See https://github.com/woocommerce/woocommerce/issues/44231
* @since 9.2 Uses wc_reserve_stock_for_order() instead of using the ReserveStock class directly.
*/
try {
wc_reserve_stock_for_order( $this->order );
} catch ( ReserveStockException $e ) {
throw new RouteException(
esc_html( $e->getErrorCode() ),
esc_html( $e->getMessage() ),
esc_html( $e->getCode() )
);
}
wc_do_deprecated_action(
'__experimental_woocommerce_blocks_checkout_order_processed',
array(
@ -402,24 +425,6 @@ class Checkout extends AbstractCartRoute {
// Store order ID to session.
$this->set_draft_order_id( $this->order->get_id() );
/**
* Try to reserve stock for the order.
*
* If creating a draft order on checkout entry, set the timeout to 10 mins.
* If POSTing to the checkout (attempting to pay), set the timeout to 60 mins (using the woocommerce_hold_stock_minutes option).
*/
try {
$reserve_stock = new ReserveStock();
$duration = $request->get_method() === 'POST' ? (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) : 10;
$reserve_stock->reserve_stock_for_order( $this->order, $duration );
} catch ( ReserveStockException $e ) {
throw new RouteException(
$e->getErrorCode(),
$e->getMessage(),
$e->getCode()
);
}
}
/**