Commit 139d2f2a2f8 for woocommerce

commit 139d2f2a2f856377cb3a5fe029ccedc10ce027f2
Author: Alefe Souza <contact@alefesouza.com>
Date:   Thu Oct 1 12:38:43 2026 -0300

    Fix offline gateway shipping restrictions on the order-pay endpoint (#68976)

diff --git a/plugins/woocommerce/changelog/fix-order-pay-query-var-offline-gateway-shipping-restrictions b/plugins/woocommerce/changelog/fix-order-pay-query-var-offline-gateway-shipping-restrictions
new file mode 100644
index 00000000000..142665877f0
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-order-pay-query-var-offline-gateway-shipping-restrictions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Always apply the offline payment methods' "Enable for shipping methods" setting at checkout, including on requests that reference an order.
diff --git a/plugins/woocommerce/src/Gateways/ShippingMethodRestrictionsTrait.php b/plugins/woocommerce/src/Gateways/ShippingMethodRestrictionsTrait.php
index 6817d63590b..c7278faa448 100644
--- a/plugins/woocommerce/src/Gateways/ShippingMethodRestrictionsTrait.php
+++ b/plugins/woocommerce/src/Gateways/ShippingMethodRestrictionsTrait.php
@@ -90,7 +90,7 @@ trait ShippingMethodRestrictionsTrait {
 	 * Check If The Gateway Is Available For Use.
 	 *
 	 * @since 10.7.0 Added early return when gateway is disabled.
-	 * @since 11.3.0 Moved here from WC_Gateway_COD.
+	 * @since 11.3.0 Moved here from WC_Gateway_COD. Only uses the order-pay context on pay-for-order page loads for an existing order.
 	 *
 	 * @return bool
 	 */
@@ -102,10 +102,13 @@ trait ShippingMethodRestrictionsTrait {
 		$is_virtual       = true;
 		$shipping_methods = array();

-		// Get shipping methods from the cart or order.
-		if ( is_wc_endpoint_url( 'order-pay' ) ) {
-			$order            = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
-			$shipping_methods = $order ? $order->get_shipping_methods() : array();
+		// Get shipping methods from the order being paid, or from the cart. The order-pay query var can be set by
+		// the client, and wc-ajax checkout requests always check out the cart, so ignore it on those requests.
+		$is_order_pay = is_wc_endpoint_url( 'order-pay' ) && ! Constants::is_true( 'WC_DOING_AJAX' );
+		$order        = $is_order_pay ? wc_get_order( absint( get_query_var( 'order-pay' ) ) ) : false;
+
+		if ( $order ) {
+			$shipping_methods = $order->get_shipping_methods();
 			$is_virtual       = ! count( $shipping_methods );
 		} elseif ( WC()->cart && WC()->cart->needs_shipping() ) {
 			$shipping_methods = WC()->cart->get_shipping_methods();
diff --git a/plugins/woocommerce/tests/php/src/Gateways/ShippingMethodRestrictionsTraitTest.php b/plugins/woocommerce/tests/php/src/Gateways/ShippingMethodRestrictionsTraitTest.php
index 07819243536..46cffe83382 100644
--- a/plugins/woocommerce/tests/php/src/Gateways/ShippingMethodRestrictionsTraitTest.php
+++ b/plugins/woocommerce/tests/php/src/Gateways/ShippingMethodRestrictionsTraitTest.php
@@ -3,6 +3,7 @@ declare( strict_types = 1 );

 namespace Automattic\WooCommerce\Tests\Gateways;

+use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\Blocks\Shipping\PickupLocation;
 use WC_Cache_Helper;
 use WC_Gateway_BACS;
@@ -10,6 +11,8 @@ use WC_Gateway_Cheque;
 use WC_Gateway_COD;
 use WC_Helper_Product;
 use WC_Helper_Shipping;
+use WC_Order;
+use WC_Order_Item_Shipping;
 use WC_Payment_Gateway;
 use WC_Shipping_Zone;
 use WC_Unit_Test_Case;
@@ -70,6 +73,8 @@ class ShippingMethodRestrictionsTraitTest extends WC_Unit_Test_Case {
 	 */
 	public function tearDown(): void {
 		try {
+			$this->set_order_pay_query_var( null );
+			Constants::clear_single_constant( 'WC_DOING_AJAX' );
 			WC()->session->set( 'chosen_shipping_methods', null );
 			WC_Cache_Helper::get_transient_version( 'shipping', true );
 			WC()->shipping()->enabled = $this->shipping_was_enabled;
@@ -187,6 +192,101 @@ class ShippingMethodRestrictionsTraitTest extends WC_Unit_Test_Case {
 		$this->assertSame( $expected, $gateway->is_available() );
 	}

+	/**
+	 * @testdox Should evaluate the cart's shipping method when the request is not paying for an order.
+	 * @testWith ["0", "flat_rate_a", true]
+	 *           ["0", "flat_rate_b", false]
+	 *           ["999999999", "flat_rate_b", false]
+	 *
+	 * @param string $order_pay   Order-pay query var value sent with the request.
+	 * @param string $chosen_rate Symbolic name of the shipping rate selected in the cart.
+	 * @param bool   $expected    Expected availability.
+	 */
+	public function test_is_available_uses_the_cart_when_order_pay_does_not_resolve( string $order_pay, string $chosen_rate, bool $expected ): void {
+		$gateway = $this->create_gateway(
+			WC_Gateway_COD::class,
+			array(
+				'enabled'            => 'yes',
+				'enable_for_methods' => array( $this->rate_ids['flat_rate_a'] ),
+				'enable_for_virtual' => 'yes',
+			)
+		);
+		$this->fill_cart( $chosen_rate );
+		$this->set_order_pay_query_var( $order_pay );
+
+		$this->assertSame( $expected, $gateway->is_available() );
+	}
+
+	/**
+	 * @testdox Should treat a cart that needs shipping as physical when the request is not paying for an order.
+	 */
+	public function test_is_available_does_not_treat_the_cart_as_virtual_when_order_pay_does_not_resolve(): void {
+		$gateway = $this->create_gateway(
+			WC_Gateway_BACS::class,
+			array(
+				'enabled'            => 'yes',
+				'enable_for_methods' => array( $this->rate_ids['flat_rate_a'] ),
+				'enable_for_virtual' => 'no',
+			)
+		);
+		$this->fill_cart( 'flat_rate_a' );
+		$this->set_order_pay_query_var( '0' );
+
+		$this->assertTrue( $gateway->is_available() );
+	}
+
+	/**
+	 * @testdox Should evaluate the order's shipping methods instead of the cart's on the order-pay page.
+	 * @testWith ["flat_rate_a", "yes", true]
+	 *           ["flat_rate_b", "yes", false]
+	 *           [null, "yes", true]
+	 *           [null, "no", false]
+	 *
+	 * @param string|null $order_rate         Symbolic name of the order's shipping rate, or null for an order without shipping.
+	 * @param string      $enable_for_virtual Saved setting value.
+	 * @param bool        $expected           Expected availability.
+	 */
+	public function test_is_available_uses_order_shipping_methods_on_order_pay_page( ?string $order_rate, string $enable_for_virtual, bool $expected ): void {
+		$gateway = $this->create_gateway(
+			WC_Gateway_Cheque::class,
+			array(
+				'enabled'            => 'yes',
+				'enable_for_methods' => array( $this->rate_ids['flat_rate_a'] ),
+				'enable_for_virtual' => $enable_for_virtual,
+			)
+		);
+		// The cart would allow the gateway on its own, so a pass here proves the order context was used.
+		$this->fill_cart( 'flat_rate_a' );
+		$this->set_order_pay_query_var( (string) $this->create_order_with_shipping( $order_rate )->get_id() );
+
+		$this->assertSame( $expected, $gateway->is_available() );
+	}
+
+	/**
+	 * @testdox Should evaluate the cart's shipping method on wc-ajax requests, even when order-pay is an existing order.
+	 * @testWith ["flat_rate_a", true]
+	 *           ["flat_rate_b", false]
+	 *
+	 * @param string $chosen_rate Symbolic name of the shipping rate selected in the cart.
+	 * @param bool   $expected    Expected availability.
+	 */
+	public function test_is_available_uses_the_cart_on_wc_ajax_requests_with_an_existing_order( string $chosen_rate, bool $expected ): void {
+		$gateway = $this->create_gateway(
+			WC_Gateway_COD::class,
+			array(
+				'enabled'            => 'yes',
+				'enable_for_methods' => array( $this->rate_ids['flat_rate_a'] ),
+				'enable_for_virtual' => 'yes',
+			)
+		);
+		$this->fill_cart( $chosen_rate );
+		// An order without shipping would count as virtual and skip the restriction if its context were used.
+		$this->set_order_pay_query_var( (string) $this->create_order_with_shipping( null )->get_id() );
+		Constants::set_constant( 'WC_DOING_AJAX', true );
+
+		$this->assertSame( $expected, $gateway->is_available() );
+	}
+
 	/**
 	 * @testdox Should not consult the cart when the gateway is disabled.
 	 * @dataProvider gateway_classes
@@ -315,6 +415,47 @@ class ShippingMethodRestrictionsTraitTest extends WC_Unit_Test_Case {
 		return $this->rate_ids[ $name ] ?? $name;
 	}

+	/**
+	 * Set the order-pay endpoint context, as WC_Query::parse_request() does when routing a request.
+	 *
+	 * @param string|null $value Query var value, or null to leave the endpoint context.
+	 */
+	private function set_order_pay_query_var( ?string $value ): void {
+		global $wp;
+
+		if ( null === $value ) {
+			unset( $wp->query_vars['order-pay'] );
+			unset( $GLOBALS['wp_query']->query_vars['order-pay'] );
+			return;
+		}
+
+		$wp->query_vars['order-pay'] = $value;
+		set_query_var( 'order-pay', $value );
+	}
+
+	/**
+	 * Create an order with a shipping line for the given rate, or without shipping.
+	 *
+	 * @param string|null $rate Symbolic name of the rate, or null for no shipping line.
+	 * @return WC_Order
+	 */
+	private function create_order_with_shipping( ?string $rate ): WC_Order {
+		$order = new WC_Order();
+
+		if ( null !== $rate ) {
+			list( $method_id, $instance_id ) = explode( ':', $this->rate_ids[ $rate ] );
+
+			$shipping_item = new WC_Order_Item_Shipping();
+			$shipping_item->set_method_id( $method_id );
+			$shipping_item->set_instance_id( $instance_id );
+			$order->add_item( $shipping_item );
+		}
+
+		$order->save();
+
+		return $order;
+	}
+
 	/**
 	 * Put a product in the real cart and select a shipping rate for it.
 	 *