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.
*