Commit 869054a7b54 for woocommerce

commit 869054a7b54873816a73d612d517a43d0df96977
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date:   Fri Aug 21 18:47:59 2026 +0100

    Revert "Stop a shopper being blocked by their own stale stock reservation" (#67931)

    * Revert "Stop a shopper being blocked by their own stale stock reservation (#6…"

    This reverts commit e817ab399ecacee9ab1b064c8fe17ce8e560f47e.

    * Add changefile(s) from automation for the following project(s): woocommerce

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/67931-revert-67751-fix-67354-own-reserved-stock-grace-window b/plugins/woocommerce/changelog/67931-revert-67751-fix-67354-own-reserved-stock-grace-window
new file mode 100644
index 00000000000..63202a40bd6
--- /dev/null
+++ b/plugins/woocommerce/changelog/67931-revert-67751-fix-67354-own-reserved-stock-grace-window
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore pending stock holds to prevent multiple payable orders from reserving the same stock.
diff --git a/plugins/woocommerce/changelog/fix-67354-own-reserved-stock-grace-window b/plugins/woocommerce/changelog/fix-67354-own-reserved-stock-grace-window
deleted file mode 100644
index 99a5e645688..00000000000
--- a/plugins/woocommerce/changelog/fix-67354-own-reserved-stock-grace-window
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: minor
-Type: fix
-
-Stop a shopper being blocked by their own abandoned stock reservation. A hold placed by the same shopper is excluded once it is older than a threshold, filterable via woocommerce_own_stock_hold_exclusion_threshold_minutes and defaulting to 10 minutes. Holds belonging to other shoppers are unaffected.
diff --git a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
index 01fa2d893ef..3ecf85a47bf 100644
--- a/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
+++ b/plugins/woocommerce/src/Checkout/Helpers/ReserveStock.php
@@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Checkout\Helpers;

 use Automattic\WooCommerce\Enums\OrderInternalStatus;
 use Automattic\WooCommerce\Enums\OrderItemType;
-use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Utilities\OrderUtil;
 use Automattic\WooCommerce\Internal\Orders\OrderNoteGroup;

@@ -18,23 +17,6 @@ defined( 'ABSPATH' ) || exit;
  */
 final class ReserveStock {

-	/**
-	 * Session key listing orders this shopper has taken a stock hold for.
-	 *
-	 * Holds array( 'customer' => string, 'order_ids' => int[] ), where `customer`
-	 * is the session customer id that recorded them.
-	 */
-	private const OWN_ORDERS_SESSION_KEY = 'stock_holding_orders';
-
-	/**
-	 * Upper bound on own orders considered when excluding a shopper's stale holds.
-	 *
-	 * Keeps the generated IN clause bounded. A shopper who exceeds it loses the
-	 * oldest ids from the list, and those holds simply block again until they
-	 * expire — the failure direction is blocking, never overselling.
-	 */
-	private const MAX_OWN_ORDERS = 10;
-
 	/**
 	 * Is stock reservation enabled?
 	 *
@@ -42,16 +24,6 @@ final class ReserveStock {
 	 */
 	private $enabled = true;

-	/**
-	 * Request scoped memo of a signed in customer's potential stock-holding order ids.
-	 *
-	 * A new ReserveStock is constructed for each wc_get_held_stock_quantity()
-	 * call, so the memo cannot live on the instance.
-	 *
-	 * @var array<int, int[]>
-	 */
-	private static $potential_order_ids_by_customer = array();
-
 	/**
 	 * Constructor
 	 */
@@ -177,8 +149,6 @@ final class ReserveStock {
 				foreach ( $rows as $product_id => $quantity ) {
 					$this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes );
 				}
-
-				$this->remember_order_for_shopper( $order );
 			}
 		} catch ( ReserveStockException $e ) {
 			$this->release_stock_for_order( $order );
@@ -323,8 +293,6 @@ final class ReserveStock {
 		);
 		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

-		$query .= $this->get_stale_own_holds_exclusion_clause( $product_id, $exclude_order_id );
-
 		/**
 		 * Filter: woocommerce_query_for_reserved_stock
 		 * Allows to filter the query for getting reserved stock of a product.
@@ -336,184 +304,4 @@ final class ReserveStock {
 		 */
 		return apply_filters( 'woocommerce_query_for_reserved_stock', $query, $product_id, $exclude_order_id );
 	}
-
-	/**
-	 * Builds a SQL clause excluding the current shopper's stale stock holds.
-	 *
-	 * @param int $product_id       Product whose reserved stock is being counted.
-	 * @param int $exclude_order_id Order already excluded by the caller.
-	 * @return string SQL clause, or an empty string when no holds qualify.
-	 */
-	private function get_stale_own_holds_exclusion_clause( $product_id, $exclude_order_id ): string {
-		global $wpdb;
-
-		/**
-		 * Filters how many minutes must pass before a shopper's own unpaid stock hold
-		 * stops blocking that same shopper.
-		 *
-		 * Only the shopper who placed the hold is affected. Return 0 to exclude own
-		 * holds immediately, or false to disable the exclusion and keep own holds
-		 * blocking for the full reservation window.
-		 *
-		 * @since 11.2.0
-		 *
-		 * @param int|false $threshold_minutes Threshold in minutes, or false to disable.
-		 * @param int       $product_id        Product whose reserved stock is being counted.
-		 * @param int       $exclude_order_id  Order already excluded by the caller.
-		 */
-		$threshold_minutes = apply_filters( 'woocommerce_own_stock_hold_exclusion_threshold_minutes', 10, $product_id, $exclude_order_id );
-
-		if ( false === $threshold_minutes ) {
-			return '';
-		}
-
-		$threshold_minutes = max( 0, (int) $threshold_minutes );
-
-		$order_ids = array_values( array_diff( $this->get_current_shopper_stock_holding_order_ids(), array( absint( $exclude_order_id ) ) ) );
-
-		if ( empty( $order_ids ) ) {
-			return '';
-		}
-
-		$placeholders = implode( ',', array_fill( 0, count( $order_ids ), '%d' ) );
-
-		// The `timestamp` column records when the hold was first placed, so it
-		// measures the age of the shopper's attempt rather than of the last write.
-		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- The interpolated value contains only generated %d placeholders.
-		return $wpdb->prepare(
-			"
-			AND NOT (
-				stock_table.`order_id` IN ( $placeholders )
-				AND stock_table.`timestamp` < ( NOW() - INTERVAL %d MINUTE )
-			)
-			",
-			array_merge( $order_ids, array( $threshold_minutes ) )
-		);
-		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-	}
-
-	/**
-	 * Returns stock-holding order ids associated with the current shopper.
-	 *
-	 * Includes orders recorded by the current session and unpaid orders belonging
-	 * to the signed-in customer. Ownership is never inferred from anything a
-	 * shopper can type, such as the billing email.
-	 *
-	 * @return int[]
-	 */
-	private function get_current_shopper_stock_holding_order_ids(): array {
-		$session = WC()->session;
-
-		// In the admin, WP-CLI and cron there is no shopper and WC()->session is
-		// not initialised, so nothing is ever treated as the requester's own hold.
-		if ( ! $session instanceof \WC_Session ) {
-			return array();
-		}
-
-		$order_ids = $this->get_session_stock_holding_order_ids( $session );
-
-		$customer_id = get_current_user_id();
-
-		if ( $customer_id ) {
-			$order_ids = array_merge( $order_ids, $this->get_potential_stock_holding_order_ids_for_customer( $customer_id ) );
-		}
-
-		return array_values( array_filter( array_unique( $order_ids ) ) );
-	}
-
-	/**
-	 * Returns the stock-holding order ids recorded against the current session.
-	 *
-	 * The list is only trusted if it was written under this session's customer id.
-	 *
-	 * @param \WC_Session $session Session of the shopper making this request.
-	 * @return int[]
-	 */
-	private function get_session_stock_holding_order_ids( \WC_Session $session ): array {
-		$remembered = $session->get( self::OWN_ORDERS_SESSION_KEY, array() );
-
-		if ( ! is_array( $remembered ) ) {
-			return array();
-		}
-
-		if ( ! isset( $remembered['customer'], $remembered['order_ids'] ) || ! is_array( $remembered['order_ids'] ) ) {
-			return array();
-		}
-
-		// Core hands session data to a different shopper in two places (cart token
-		// cloning and guest-to-user migration), and both change the session
-		// customer id first. A mismatch means the list arrived from somebody
-		// else's session, so it confers no ownership.
-		if ( (string) $remembered['customer'] !== (string) $session->get_customer_id() ) {
-			return array();
-		}
-
-		return array_slice( array_map( 'absint', $remembered['order_ids'] ), 0, self::MAX_OWN_ORDERS );
-	}
-
-	/**
-	 * Records a stock-holding order against the current customer session.
-	 *
-	 * The session's customer id is stamped alongside the ids so the list can be
-	 * discarded if it later turns up in somebody else's session. See
-	 * get_session_stock_holding_order_ids().
-	 *
-	 * @param \WC_Order $order Order that now holds stock.
-	 */
-	private function remember_order_for_shopper( \WC_Order $order ): void {
-		$session = WC()->session;
-
-		// An order created in the admin or over WP-CLI has no session to record against.
-		if ( ! $session instanceof \WC_Session ) {
-			return;
-		}
-
-		$order_ids = $this->get_session_stock_holding_order_ids( $session );
-
-		array_unshift( $order_ids, $order->get_id() );
-
-		$order_ids = array_slice( array_values( array_unique( array_map( 'absint', $order_ids ) ) ), 0, self::MAX_OWN_ORDERS );
-
-		$session->set(
-			self::OWN_ORDERS_SESSION_KEY,
-			array(
-				'customer'  => (string) $session->get_customer_id(),
-				'order_ids' => $order_ids,
-			)
-		);
-	}
-
-	/**
-	 * Returns order ids of a signed-in customer that could be holding stock, newest first.
-	 *
-	 * Covers a shopper whose session pointers were lost (new device, cleared
-	 * cookies, lapsed session). The statuses match the ones the reserved stock
-	 * query counts.
-	 *
-	 * @param int $customer_id Customer ID.
-	 * @return int[]
-	 */
-	private function get_potential_stock_holding_order_ids_for_customer( int $customer_id ): array {
-		if ( isset( self::$potential_order_ids_by_customer[ $customer_id ] ) ) {
-			return self::$potential_order_ids_by_customer[ $customer_id ];
-		}
-
-		/** @var int[] $order_ids */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
-		$order_ids = wc_get_orders(
-			array(
-				'customer' => $customer_id,
-				'status'   => array( OrderStatus::CHECKOUT_DRAFT, OrderStatus::PENDING ),
-				'limit'    => self::MAX_OWN_ORDERS,
-				'orderby'  => 'date',
-				'order'    => 'DESC',
-				'return'   => 'ids',
-			)
-		);
-
-		$order_ids = array_map( 'absint', $order_ids );
-
-		self::$potential_order_ids_by_customer[ $customer_id ] = $order_ids;
-
-		return $order_ids;
-	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Checkout/Helpers/ReserveStockOwnHoldsTest.php b/plugins/woocommerce/tests/php/src/Checkout/Helpers/ReserveStockOwnHoldsTest.php
deleted file mode 100644
index 958c259f95b..00000000000
--- a/plugins/woocommerce/tests/php/src/Checkout/Helpers/ReserveStockOwnHoldsTest.php
+++ /dev/null
@@ -1,485 +0,0 @@
-<?php
-/**
- * Tests for excluding a shopper's own stale stock holds.
- */
-
-declare(strict_types=1);
-
-namespace Automattic\WooCommerce\Tests\Checkout\Helpers;
-
-use Automattic\WooCommerce\Enums\OrderStatus;
-use WC_Customer;
-use WC_Helper_Order;
-use WC_Helper_Product;
-use WC_Product_Simple;
-use WC_Session_Handler;
-use WC_Unit_Test_Case;
-
-/**
- * Class ReserveStockOwnHoldsTest.
- *
- * Exercises the threshold that stops a shopper being blocked by their own
- * stale, unpaid stock hold:
- *
- *   1. A hold belonging to another shopper always blocks, however old it is.
- *   2. The shopper's own hold blocks inside the threshold and is excluded past it.
- *   3. Ownership never comes from anything the shopper can type.
- *   4. With no session (admin, WP-CLI, cron) nothing is excluded.
- *   5. The woocommerce_query_for_reserved_stock signature is unchanged.
- *
- * Holds are written straight into wc_reserved_stock so `timestamp` can be aged
- * exactly. Tests exercising the session list call wc_reserve_stock_for_order()
- * for real.
- */
-class ReserveStockOwnHoldsTest extends WC_Unit_Test_Case {
-
-	/**
-	 * How long the reservation itself lasts, well beyond any threshold used here.
-	 */
-	private const HOLD_MINUTES = 2880;
-
-	/**
-	 * The session the bootstrap set up, put back in tearDown.
-	 *
-	 * @var \WC_Session|null
-	 */
-	private $original_session;
-
-	/**
-	 * The customer the bootstrap set up, put back in tearDown.
-	 *
-	 * @var WC_Customer|null
-	 */
-	private $original_customer;
-
-	/**
-	 * $_COOKIE as it was before the test, put back in tearDown.
-	 *
-	 * @var array
-	 */
-	private $original_cookies;
-
-	/**
-	 * Set up a shopper session and a store that manages stock.
-	 */
-	public function setUp(): void {
-		parent::setUp();
-
-		update_option( 'woocommerce_manage_stock', 'yes' );
-		update_option( 'woocommerce_schema_version', 430 );
-		update_option( 'woocommerce_hold_stock_minutes', self::HOLD_MINUTES );
-
-		$this->original_session  = WC()->session;
-		$this->original_customer = WC()->customer;
-
-		// Earlier suites can leave a valid session cookie in $_COOKIE (the test
-		// framework never resets it), and WC_Session_Handler::init() would then
-		// give every session in these tests that cookie's customer id instead of
-		// minting distinct ones.
-		$this->original_cookies = $_COOKIE;
-
-		/** This filter is documented in includes/class-wc-session-handler.php */
-		unset( $_COOKIE[ (string) apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ) ] );
-
-		WC()->session = new WC_Session_Handler();
-		WC()->session->init();
-	}
-
-	/**
-	 * Leave no session, user or reservation behind.
-	 *
-	 * The session and customer singletons are restored rather than nulled: this
-	 * suite runs in one process, and a null WC()->session fatals later classes.
-	 */
-	public function tearDown(): void {
-		global $wpdb;
-
-		$wpdb->query( "DELETE FROM {$wpdb->wc_reserved_stock}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-
-		wp_set_current_user( 0 );
-
-		$_COOKIE       = $this->original_cookies;
-		WC()->session  = $this->original_session;
-		WC()->customer = $this->original_customer;
-
-		parent::tearDown();
-	}
-
-	// ---------------------------------------------------------------------
-	// 1. The oversell invariant.
-	// ---------------------------------------------------------------------
-
-	/**
-	 * A stale hold on an order the shopper does not own keeps blocking, even
-	 * while the shopper's own stale hold is excluded.
-	 */
-	public function test_own_stale_hold_does_not_exclude_another_shoppers_hold() {
-		$product = $this->create_stock_managed_product( 2 );
-		$shopper = $this->create_signed_in_shopper();
-		$mine    = $this->create_unpaid_order_for( $shopper, $product );
-		$theirs  = $this->create_shopper_with_unpaid_order( $product );
-
-		$this->hold_stock( $mine, $product->get_id(), 1, 120 );
-		$this->hold_stock( $theirs, $product->get_id(), 1, 120 );
-
-		$this->assertSame( 1, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	// ---------------------------------------------------------------------
-	// 2. The exclusion threshold.
-	// ---------------------------------------------------------------------
-
-	/**
-	 * The shopper's own hold still blocks inside the default 10 minute threshold.
-	 */
-	public function test_own_fresh_hold_still_blocks() {
-		$product = $this->create_stock_managed_product( 1 );
-		$shopper = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $shopper, $product );
-
-		$this->hold_stock( $order, $product->get_id(), 1, 9 );
-
-		$this->assertSame( 1, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	/**
-	 * Past the threshold the shopper stops being blocked by their own attempt.
-	 */
-	public function test_own_stale_hold_is_excluded() {
-		$product = $this->create_stock_managed_product( 1 );
-		$shopper = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $shopper, $product );
-
-		$this->hold_stock( $order, $product->get_id(), 1, 11 );
-
-		$this->assertSame( 0, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	/**
-	 * A false threshold disables the exclusion and restores the behaviour from
-	 * before this change.
-	 */
-	public function test_false_threshold_keeps_own_hold_blocking() {
-		$product = $this->create_stock_managed_product( 1 );
-		$shopper = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $shopper, $product );
-
-		$this->hold_stock( $order, $product->get_id(), 1, 2000 );
-
-		add_filter( 'woocommerce_own_stock_hold_exclusion_threshold_minutes', '__return_false' );
-		try {
-			$held = wc_get_held_stock_quantity( $product, 0 );
-		} finally {
-			remove_filter( 'woocommerce_own_stock_hold_exclusion_threshold_minutes', '__return_false' );
-		}
-
-		$this->assertSame( 1, $held );
-	}
-
-	/**
-	 * A threshold of zero excludes the shopper's own hold immediately.
-	 */
-	public function test_zero_threshold_excludes_own_hold_immediately() {
-		$product = $this->create_stock_managed_product( 1 );
-		$shopper = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $shopper, $product );
-
-		$this->hold_stock( $order, $product->get_id(), 1, 5 );
-
-		add_filter( 'woocommerce_own_stock_hold_exclusion_threshold_minutes', '__return_zero' );
-		try {
-			$held = wc_get_held_stock_quantity( $product, 0 );
-		} finally {
-			remove_filter( 'woocommerce_own_stock_hold_exclusion_threshold_minutes', '__return_zero' );
-		}
-
-		$this->assertSame( 0, $held );
-	}
-
-	// ---------------------------------------------------------------------
-	// 3. Ownership sources.
-	// ---------------------------------------------------------------------
-
-	/**
-	 * The reported flow: a guest on the block checkout whose draft pointer has
-	 * already moved on to a new draft order.
-	 */
-	public function test_session_list_covers_a_guest_whose_draft_pointer_moved_on() {
-		$product = $this->create_stock_managed_product( 1 );
-		$order   = $this->create_unpaid_order_for( 0, $product );
-
-		// The real reservation path, which is what records the order in the session.
-		wc_reserve_stock_for_order( wc_get_order( $order ) );
-		$this->age_hold( $order, 11 );
-
-		// The Store API has since minted a new draft and repointed the session at it.
-		$new_draft = $this->create_unpaid_order_for( 0, $product, OrderStatus::CHECKOUT_DRAFT );
-		WC()->session->set( 'store_api_draft_order', $new_draft );
-
-		$this->assertSame( 0, wc_get_held_stock_quantity( $product, $new_draft ) );
-	}
-
-	/**
-	 * A session list that arrived from somebody else's session confers nothing.
-	 *
-	 * Core hands session data to a different shopper in two places (cart token
-	 * cloning and guest-to-user migration); both change the session customer id
-	 * first, which is what the stamp detects.
-	 */
-	public function test_borrowed_session_list_confers_no_ownership() {
-		$product = $this->create_stock_managed_product( 1 );
-		$order   = $this->create_unpaid_order_for( 0, $product );
-
-		wc_reserve_stock_for_order( wc_get_order( $order ) );
-		$this->age_hold( $order, 11 );
-
-		$borrowed = WC()->session->get( 'stock_holding_orders', array() );
-
-		// A second shopper, whose session carries the first shopper's list verbatim
-		// but under their own, different customer id.
-		WC()->session = new WC_Session_Handler();
-		WC()->session->init();
-		WC()->session->set( 'stock_holding_orders', $borrowed );
-
-		$this->assertNotSame(
-			$borrowed['customer'],
-			(string) WC()->session->get_customer_id(),
-			'Precondition: the borrowing session has a different customer id.'
-		);
-		$this->assertSame( 1, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	/**
-	 * A billing email is not an ownership claim.
-	 *
-	 * The attacker types the victim's address into their own checkout. The
-	 * victim's in-flight hold must keep blocking.
-	 */
-	public function test_billing_email_confers_no_ownership() {
-		$product      = $this->create_stock_managed_product( 1 );
-		$victim       = $this->create_signed_in_shopper( 'victim@example.com' );
-		$order        = $this->create_unpaid_order_for( $victim, $product );
-		$victim_order = wc_get_order( $order );
-		$victim_order->set_billing_email( 'victim@example.com' );
-		$victim_order->save();
-
-		$this->hold_stock( $order, $product->get_id(), 1, 120 );
-
-		// Now a different, unrelated shopper claiming the same address.
-		wp_set_current_user( 0 );
-		WC()->session = new WC_Session_Handler();
-		WC()->session->init();
-		WC()->customer = new WC_Customer();
-		WC()->customer->set_billing_email( 'victim@example.com' );
-
-		$this->assertSame( 1, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	/**
-	 * A guest who exceeds the session list limit loses the oldest entry, and that
-	 * hold blocks again: the failure direction is blocking, never overselling.
-	 */
-	public function test_own_holds_beyond_the_session_list_limit_block_again() {
-		$product = $this->create_stock_managed_product( 20 );
-
-		// One more attempt than the list keeps.
-		for ( $i = 0; $i < 11; $i++ ) {
-			$order = $this->create_unpaid_order_for( 0, $product );
-			wc_reserve_stock_for_order( wc_get_order( $order ) );
-			$this->age_hold( $order, 11 );
-		}
-
-		// The list keeps the ten newest ids, so the first order's hold counts
-		// again while the remembered ten are excluded.
-		$this->assertSame( 1, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	// ---------------------------------------------------------------------
-	// 4. Contexts with no shopper.
-	// ---------------------------------------------------------------------
-
-	/**
-	 * With no session — the admin, WP-CLI, cron — nothing is excluded, even for
-	 * a signed in user who happens to have unpaid orders of their own.
-	 */
-	public function test_no_session_leaves_the_total_unchanged() {
-		$product = $this->create_stock_managed_product( 1 );
-		$shopper = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $shopper, $product );
-
-		$this->hold_stock( $order, $product->get_id(), 1, 120 );
-
-		WC()->session = null;
-
-		$this->assertSame( 1, wc_get_held_stock_quantity( $product, 0 ) );
-	}
-
-	// ---------------------------------------------------------------------
-	// 5. The public filter contract.
-	// ---------------------------------------------------------------------
-
-	/**
-	 * woocommerce_query_for_reserved_stock still receives a scalar order id, and
-	 * still receives the complete core query, exclusion clause included.
-	 */
-	public function test_filter_signature_is_unchanged() {
-		$product = $this->create_stock_managed_product( 1 );
-		$shopper = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $shopper, $product );
-
-		$this->hold_stock( $order, $product->get_id(), 1, 120 );
-
-		$seen = array();
-
-		$capture = function ( $query, $product_id, $exclude_order_id ) use ( &$seen ) {
-			$seen = array(
-				'query'            => $query,
-				'product_id'       => $product_id,
-				'exclude_order_id' => $exclude_order_id,
-			);
-
-			return $query;
-		};
-
-		add_filter( 'woocommerce_query_for_reserved_stock', $capture, 10, 3 );
-		try {
-			wc_get_held_stock_quantity( $product, 4242 );
-		} finally {
-			remove_filter( 'woocommerce_query_for_reserved_stock', $capture, 10 );
-		}
-
-		$this->assertIsInt( $seen['exclude_order_id'] );
-		$this->assertSame( 4242, $seen['exclude_order_id'] );
-		$this->assertSame( $product->get_id(), $seen['product_id'] );
-		$this->assertStringContainsString( 'AND NOT (', $seen['query'], 'The filter sees the complete core query.' );
-	}
-
-	// ---------------------------------------------------------------------
-	// Helpers.
-	// ---------------------------------------------------------------------
-
-	/**
-	 * A simple product managing a fixed quantity of stock.
-	 *
-	 * @param int $quantity Stock quantity.
-	 * @return WC_Product_Simple
-	 */
-	private function create_stock_managed_product( int $quantity ): WC_Product_Simple {
-		$product = WC_Helper_Product::create_simple_product();
-		$product->set_manage_stock( true );
-		$product->set_stock_quantity( $quantity );
-		$product->save();
-
-		return $product;
-	}
-
-	/**
-	 * A signed in shopper. A fresh user per test, because the customer lookup is
-	 * memoised for the life of the request.
-	 *
-	 * @param string $email Optional email.
-	 * @return int User ID.
-	 */
-	private function create_signed_in_shopper( string $email = '' ): int {
-		$user_id = wp_insert_user(
-			array(
-				'user_login' => uniqid( 'shopper_' ),
-				'user_pass'  => wp_generate_password(),
-				'user_email' => '' !== $email ? $email : uniqid( 'shopper_' ) . '@example.com',
-				'role'       => 'customer',
-			)
-		);
-
-		wp_set_current_user( $user_id );
-
-		return (int) $user_id;
-	}
-
-	/**
-	 * An unpaid order for a customer, holding one unit of the product.
-	 *
-	 * WC_Helper_Order::create_order() hard-codes a line quantity of 4, so the
-	 * line item is rebuilt — with a save() between remove and add, without which
-	 * the order reloads with no items and reserves nothing.
-	 *
-	 * @param int               $customer_id Customer ID, 0 for a guest.
-	 * @param WC_Product_Simple $product     Product to add.
-	 * @param string            $status      Order status, unpaid pending by default.
-	 * @return int Order ID.
-	 */
-	private function create_unpaid_order_for( int $customer_id, WC_Product_Simple $product, string $status = OrderStatus::PENDING ): int {
-		$order = WC_Helper_Order::create_order( $customer_id );
-		$order->remove_order_items();
-		$order->save();
-		$order->add_product( wc_get_product( $product->get_id() ), 1 );
-		$order->set_status( $status );
-		$order->save();
-
-		return $order->get_id();
-	}
-
-	/**
-	 * An unpaid order belonging to somebody who is not the current user, and whose
-	 * id never enters the current session.
-	 *
-	 * @param WC_Product_Simple $product Product to add.
-	 * @return int Order ID.
-	 */
-	private function create_shopper_with_unpaid_order( WC_Product_Simple $product ): int {
-		$current = get_current_user_id();
-		$other   = $this->create_signed_in_shopper();
-		$order   = $this->create_unpaid_order_for( $other, $product );
-
-		wp_set_current_user( $current );
-
-		return $order;
-	}
-
-	/**
-	 * Write a hold directly, with an exact age.
-	 *
-	 * @param int $order_id    Order holding the stock.
-	 * @param int $product_id  Product held.
-	 * @param int $quantity    Quantity held.
-	 * @param int $age_minutes How long ago the hold was placed.
-	 */
-	private function hold_stock( int $order_id, int $product_id, int $quantity, int $age_minutes ): void {
-		global $wpdb;
-
-		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-		$wpdb->query(
-			$wpdb->prepare(
-				"
-				INSERT INTO {$wpdb->wc_reserved_stock} ( `order_id`, `product_id`, `stock_quantity`, `timestamp`, `expires` )
-				VALUES ( %d, %d, %d, ( NOW() - INTERVAL %d MINUTE ), ( NOW() + INTERVAL %d MINUTE ) )
-				",
-				$order_id,
-				$product_id,
-				$quantity,
-				$age_minutes,
-				self::HOLD_MINUTES
-			)
-		);
-		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-	}
-
-	/**
-	 * Backdate a hold that was created by the real reservation path.
-	 *
-	 * @param int $order_id    Order holding the stock.
-	 * @param int $age_minutes How long ago the hold should read as placed.
-	 */
-	private function age_hold( int $order_id, int $age_minutes ): void {
-		global $wpdb;
-
-		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-		$wpdb->query(
-			$wpdb->prepare(
-				"UPDATE {$wpdb->wc_reserved_stock} SET `timestamp` = ( NOW() - INTERVAL %d MINUTE ) WHERE `order_id` = %d",
-				$age_minutes,
-				$order_id
-			)
-		);
-		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-	}
-}