Commit 81c0b130d11 for woocommerce
commit 81c0b130d11b3d4f113ff17bc6535ec7f2537bea
Author: Mike Jolley <mike.jolley@me.com>
Date: Thu Oct 22 16:47:17 2026 +0100
Show shipping when forced-billing stores remove billing address fields (#69930)
* Check billing fields when hiding shipping for forced-billing stores
* Add changelog entry for the forced-billing hidden shipping fix
diff --git a/plugins/woocommerce/changelog/69737-forced-billing-removed-address-fields b/plugins/woocommerce/changelog/69737-forced-billing-removed-address-fields
new file mode 100644
index 00000000000..e69352c2d71
--- /dev/null
+++ b/plugins/woocommerce/changelog/69737-forced-billing-removed-address-fields
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Charge shipping at the classic checkout when shipping is forced to billing and the store removed billing address fields.
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index cdfdaf08b08..76f7327a2d1 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -1917,7 +1917,10 @@ class WC_Cart extends WC_Legacy_Cart {
$state_enabled = apply_filters( 'woocommerce_shipping_calculator_enable_state', true );
$state_required = isset( $country_fields['shipping_state'] ) && $country_fields['shipping_state']['required'] && true !== ( $country_fields['shipping_state']['hidden'] ?? false );
// Takes care of late unsetting of checkout fields via hooks (woocommerce_checkout_fields, woocommerce_shipping_fields).
- $checkout_state_field_exists = isset( $checkout_fields['shipping']['shipping_state'] );
+ // When shipping is forced to the billing address the shipping fields are never shown, so only a field the shopper
+ // can actually fill in (the billing one) should keep shipping hidden.
+ $address_fieldset = wc_ship_to_billing_address_only() ? 'billing' : 'shipping';
+ $checkout_state_field_exists = isset( $checkout_fields[ $address_fieldset ][ $address_fieldset . '_state' ] );
if ( $state_enabled && $state_required && ! $this->get_customer()->get_shipping_state() && $checkout_state_field_exists ) {
return false;
}
@@ -1932,7 +1935,7 @@ class WC_Cart extends WC_Legacy_Cart {
$postcode_enabled = apply_filters( 'woocommerce_shipping_calculator_enable_postcode', true );
$postcode_required = isset( $country_fields['shipping_postcode'] ) && $country_fields['shipping_postcode']['required'] && true !== ( $country_fields['shipping_postcode']['hidden'] ?? false );
// Takes care of late unsetting of checkout fields via hooks (woocommerce_checkout_fields, woocommerce_shipping_fields).
- $checkout_postcode_field_exists = isset( $checkout_fields['shipping']['shipping_postcode'] );
+ $checkout_postcode_field_exists = isset( $checkout_fields[ $address_fieldset ][ $address_fieldset . '_postcode' ] );
if ( $postcode_enabled && $postcode_required && '' === $this->get_customer()->get_shipping_postcode() && $checkout_postcode_field_exists ) {
return false;
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index af77d9ef44b..8b77a9b45df 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -1096,6 +1096,40 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
WC()->cart->get_customer()->set_shipping_postcode( '' );
}
+ /**
+ * @testdox show_shipping() checks the billing fields when shipping is forced to the billing address, so a removed billing postcode no longer hides shipping.
+ *
+ * @testWith [ "billing_only", true ]
+ * [ "billing", false ]
+ * [ "shipping", false ]
+ *
+ * @param string $ship_to_destination Value of the woocommerce_ship_to_destination option.
+ * @param bool $expected Expected show_shipping() result with an empty postcode.
+ */
+ public function test_show_shipping_reads_billing_fields_when_shipping_is_forced_to_billing( string $ship_to_destination, bool $expected ): 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' );
+ update_option( 'woocommerce_ship_to_destination', $ship_to_destination );
+
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+ WC()->customer->set_shipping_location( 'US', 'NY', '', '' );
+
+ try {
+ $this->assertSame( $expected, WC()->cart->show_shipping(), 'Shipping should only stay hidden while the shopper has a postcode field to fill in.' );
+
+ WC()->customer->set_shipping_location( 'US', '', '', '' );
+ $this->assertFalse( WC()->cart->show_shipping(), 'A removed postcode must not stop the state from being required.' );
+ } finally {
+ remove_filter( 'woocommerce_checkout_fields', $remove_billing_address );
+ WC()->customer->set_shipping_location( '', '', '', '' );
+ }
+ }
+
/**
* Test that show_shipping returns true when Local Pickup is enabled,
* even when "Hide shipping costs until an address is entered" is enabled
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 2d251848e52..d23bf0dd035 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-checkout-test.php
@@ -786,6 +786,59 @@ class WC_Checkout_Test extends \WC_Unit_Test_Case {
);
}
+ /**
+ * @testdox 'validate_checkout' charges shipping when shipping is forced to billing and the removed billing fields are the only missing address parts.
+ *
+ * @testWith [ "10001", "O'Fallon" ]
+ * [ "", "" ]
+ *
+ * @param string $calculator_postcode Postcode entered in the cart shipping calculator, or empty when the shopper went straight to checkout.
+ * @param string $calculator_city City entered in the cart shipping calculator.
+ */
+ public function test_validate_checkout_accepts_forced_billing_order_when_removed_billing_fields_are_missing( string $calculator_postcode, string $calculator_city ): 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' );
+ update_option( 'woocommerce_ship_to_destination', 'billing_only' );
+ 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', $calculator_postcode, $calculator_city );
+ WC()->cart->calculate_totals();
+
+ $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',
+ 'shipping_method' => array( 'flat_rate' ),
+ '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 );
+ WC_Helper_Shipping::delete_simple_flat_rate();
+ }
+
+ $this->assertTrue( WC()->cart->has_calculated_shipping(), 'Shipping should be calculated from the country and state the shopper was asked for.' );
+ $this->assertSame( '', $errors->get_error_message( 'shipping' ), 'The order must not be blocked for an address field the store does not show.' );
+ $this->assertSame( 8.0, (float) WC()->cart->get_shipping_total(), 'The order must be charged the shipping rate.' );
+ }
+
/**
* @testdox 'validate_checkout' still accepts an order when a plugin deliberately turns off shipping calculation.
*/