Commit 192db7fb636 for woocommerce
commit 192db7fb6369505f3e593531cb12358fea5b6549
Author: Defyn <99234555+Defyn-Digital@users.noreply.github.com>
Date: Thu Oct 15 17:50:28 2026 +0545
Fix/69737 checkout without shipping package (#69742)
* Refuse classic checkout orders when shipping was withheld for a missing address
With "Hide shipping costs until an address is entered" on, a store that
removes billing address fields gets those fields copied into the shipping
address as empty strings when the order is placed. show_shipping() then
skips shipping, and validate_checkout() only checked packages that exist,
so the order saved with no shipping line and a lower total than the
shopper was shown.
Add a guard in validate_checkout(): when the cart needs shipping, the
setting is on, and shipping was not calculated, return the existing
"No shipping method has been selected" error. Stores that turn off
shipping calculation through woocommerce_cart_ready_to_calc_shipping
are left alone.
* Add changelog entry for classic checkout shipping guard
* Ask the shopper for an address when shipping was withheld
The order review still shows the shipping rate after the error, and the
shopper may have no address fields to double check, so the existing
"Please enter an address to continue." string fits better.
---------
Co-authored-by: Mike Jolley <mike.jolley@me.com>
diff --git a/plugins/woocommerce/changelog/69737-checkout-without-shipping-package b/plugins/woocommerce/changelog/69737-checkout-without-shipping-package
new file mode 100644
index 00000000000..6c78bf599f3
--- /dev/null
+++ b/plugins/woocommerce/changelog/69737-checkout-without-shipping-package
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop the classic checkout saving orders without shipping when shipping costs are hidden until an address is entered.
diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index 56803eb4851..d13acbb38e7 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -1046,6 +1046,8 @@ class WC_Checkout {
)
);
}
+ } elseif ( $this->is_shipping_withheld_for_missing_address() ) {
+ $errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) );
} else {
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
@@ -1070,6 +1072,28 @@ class WC_Checkout {
do_action( 'woocommerce_after_checkout_validation', $data, $errors );
}
+ /**
+ * Check whether shipping was left out because "Hide shipping costs until an address is entered" found the address incomplete.
+ *
+ * Without this check an order could be saved with no shipping line, for example when a store removes billing
+ * address fields and the empty values are copied over the shipping address. Stores that turn off shipping
+ * calculation through the woocommerce_cart_ready_to_calc_shipping filter are left alone.
+ *
+ * @return bool
+ */
+ private function is_shipping_withheld_for_missing_address(): bool {
+ if ( 'yes' !== get_option( 'woocommerce_shipping_cost_requires_address' ) || WC()->cart->has_calculated_shipping() ) {
+ return false;
+ }
+
+ /**
+ * This filter is documented in includes/class-wc-cart.php.
+ *
+ * @since 2.7.0
+ */
+ return (bool) apply_filters( 'woocommerce_cart_ready_to_calc_shipping', true );
+ }
+
/**
* Set address field for customer.
*
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
index a52c89d3445..e6b76fd5642 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
@@ -46,6 +46,10 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
public function process_customer( $data ) {
parent::process_customer( $data );
}
+
+ public function update_session( $data ) {
+ return parent::update_session( $data );
+ }
};
// phpcs:enable Generic.CodeAnalysis, Squiz.Commenting
@@ -731,6 +735,90 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
remove_all_filters( 'woocommerce_countries_allowed_countries' );
}
+ /**
+ * @testdox 'validate_checkout' refuses a shippable order when hidden shipping costs leave the cart without a shipping package.
+ */
+ public function test_validate_checkout_refuses_order_without_shipping_package_when_costs_need_an_address(): void {
+ $remove_billing_address = static function ( $fields ) {
+ unset( $fields['billing']['billing_city'], $fields['billing']['billing_address_1'], $fields['billing']['billing_postcode'] );
+ return $fields;
+ };
+ add_filter( 'woocommerce_checkout_fields', $remove_billing_address );
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+ WC_Helper_Shipping::create_simple_flat_rate( 8 );
+
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+
+ WC()->customer->set_shipping_location( 'US', 'NY', '10001', "O'Fallon" );
+ WC()->cart->calculate_totals();
+ $this->assertTrue( WC()->cart->has_calculated_shipping(), 'The order review should show a shipping rate once the cart calculator address is set.' );
+
+ $original_post = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Restored in the finally block.
+ $_POST = array(
+ 'billing_first_name' => 'Test',
+ 'billing_last_name' => 'Shopper',
+ 'billing_phone' => '5555555555',
+ 'billing_email' => 'shopper@example.com',
+ 'billing_country' => 'US',
+ 'billing_state' => 'NY',
+ 'payment_method' => 'cod',
+ );
+
+ try {
+ $data = $this->sut->get_posted_data();
+ $this->sut->update_session( $data );
+
+ $errors = new WP_Error();
+ $this->sut->validate_checkout( $data, $errors );
+ } finally {
+ $_POST = $original_post;
+ remove_filter( 'woocommerce_checkout_fields', $remove_billing_address );
+ delete_option( 'woocommerce_shipping_cost_requires_address' );
+ WC_Helper_Shipping::delete_simple_flat_rate();
+ }
+
+ $this->assertFalse( WC()->cart->has_calculated_shipping(), 'Placing the order blanks the shipping address, so shipping is no longer calculated.' );
+ $this->assertSame(
+ 'Please enter an address to continue.',
+ $errors->get_error_message( 'shipping' ),
+ 'Checkout must not accept a shippable order that has no shipping package.'
+ );
+ }
+
+ /**
+ * @testdox 'validate_checkout' still accepts an order when a plugin deliberately turns off shipping calculation.
+ */
+ public function test_validate_checkout_accepts_order_when_shipping_calculation_is_turned_off_by_filter(): void {
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+ add_filter( 'woocommerce_cart_ready_to_calc_shipping', '__return_false' );
+ WC_Helper_Shipping::create_simple_flat_rate( 8 );
+
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+ WC()->customer->set_shipping_location( 'US', 'NY', '10001', 'New York' );
+
+ try {
+ WC()->cart->calculate_totals();
+
+ $data = array(
+ 'ship_to_different_address' => false,
+ 'billing_country' => 'US',
+ 'shipping_country' => 'US',
+ 'payment_method' => 'cod',
+ );
+ $errors = new WP_Error();
+ $this->sut->validate_checkout( $data, $errors );
+ } finally {
+ remove_filter( 'woocommerce_cart_ready_to_calc_shipping', '__return_false' );
+ delete_option( 'woocommerce_shipping_cost_requires_address' );
+ WC_Helper_Shipping::delete_simple_flat_rate();
+ }
+
+ $this->assertFalse( WC()->cart->has_calculated_shipping(), 'The filter should stop shipping from being calculated.' );
+ $this->assertSame( '', $errors->get_error_message( 'shipping' ), 'Stores that turn off shipping calculation on purpose must still be able to take orders.' );
+ }
+
/**
* @testdox If the WooCommerce class's customer object is null (like if WC has not been fully initialized yet),
* calling WC_Checkout::get_value should not throw an error.