Commit c198af5c818 for woocommerce

commit c198af5c818093b7954b4a6b0b4afe457b048e9e
Author: Chris Huber <chubes@extrachill.com>
Date:   Fri Jun 19 12:00:08 2026 -0400

    Fix shipping cache invalidation from package metadata (#65541)

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

    * Broaden shipping package cache hash exclusions

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

    * Address shipping package hash review feedback

    * Fix shipping test array alignment

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Vladimir Reznichenko <kalessil@gmail.com>

diff --git a/plugins/woocommerce/changelog/65541-fix-checkout-shipping-cache-homeboy b/plugins/woocommerce/changelog/65541-fix-checkout-shipping-cache-homeboy
new file mode 100644
index 00000000000..6cbd7111076
--- /dev/null
+++ b/plugins/woocommerce/changelog/65541-fix-checkout-shipping-cache-homeboy
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent non-rate package metadata from invalidating cached shipping rates.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php
index 0859d0cde6d..1c9e335f90e 100644
--- a/plugins/woocommerce/includes/class-wc-shipping.php
+++ b/plugins/woocommerce/includes/class-wc-shipping.php
@@ -320,20 +320,12 @@ class WC_Shipping {
 		// local pickup rates here however since those are not shipped.
 		$is_shippable = $this->is_package_shippable( $package );

-		// Check if we need to recalculate shipping for this package.
-		$package_to_hash = $package;
-
-		// Remove data objects so hashes are consistent.
-		foreach ( $package_to_hash['contents'] as $item_id => $item ) {
-			unset( $package_to_hash['contents'][ $item_id ]['data'] );
-		}
-
 		// Get rates stored in the WC session data for this package.
 		$wc_session_key = 'shipping_for_package_' . $package_key;
 		$stored_rates   = WC()->session->get( $wc_session_key );

 		// Calculate the hash for this package so we can tell if it's changed since last calculation.
-		$package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
+		$package_hash = $this->get_package_hash( $package );

 		if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) {
 			foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) {
@@ -418,6 +410,46 @@ class WC_Shipping {
 		return $package;
 	}

+	/**
+	 * Generate the shipping-rate cache hash for a package.
+	 *
+	 * @param array $package_to_hash Package of cart items.
+	 * @return string
+	 */
+	private function get_package_hash( array $package_to_hash ): string {
+		/**
+		 * Filters package fields that should not affect the shipping-rate cache hash.
+		 *
+		 * @since 11.0.0
+		 *
+		 * @param array $ignored_fields Package field names ignored while generating the cache hash.
+		 * @param array $package        Package of cart items.
+		 */
+		$ignored_fields = apply_filters(
+			'woocommerce_shipping_package_hash_ignored_fields',
+			array(
+				'subtotal',
+				'total',
+				'package_id',
+				'package_name',
+				'rates',
+				'package_index',
+			),
+			$package_to_hash
+		);
+
+		foreach ( array_unique( array_filter( (array) $ignored_fields, 'is_string' ) ) as $field ) {
+			unset( $package_to_hash[ $field ] );
+		}
+
+		// Remove data objects so hashes are consistent.
+		foreach ( (array) ( $package_to_hash['contents'] ?? array() ) as $item_id => $item ) {
+			unset( $package_to_hash['contents'][ $item_id ]['data'] );
+		}
+
+		return 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) );
+	}
+
 	/**
 	 * Get packages.
 	 *
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php b/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
index fbe43a11620..535bf37d22e 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
@@ -128,6 +128,117 @@ class WC_Shipping_Test extends WC_Unit_Test_Case {
 		remove_action( 'woocommerce_shipping_methods', $shipping_methods_hook );
 	}

+	/**
+	 * @testdox ignored package fields do not invalidate cached shipping rates
+	 *
+	 * @dataProvider provide_ignored_package_hash_fields
+	 *
+	 * @param string $field Package field to mutate.
+	 * @param mixed  $value Mutated field value.
+	 */
+	public function test_calculate_shipping_for_package_ignores_non_rate_fields_in_package_hash( string $field, $value ) {
+		update_option( 'woocommerce_shipping_debug_mode', 'no' );
+		WC()->session->__unset( 'shipping_for_package_0' );
+
+		$filter_calls = 0;
+		$filter       = $this->get_package_rates_counter( $filter_calls );
+		$package      = $this->get_package_hash_test_package();
+
+		add_filter( 'woocommerce_package_rates', $filter, 10 );
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$package[ $field ] = $value;
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$this->assertSame( 1, $filter_calls );
+
+		remove_filter( 'woocommerce_package_rates', $filter, 10 );
+	}
+
+	/**
+	 * @testdox material package fields invalidate cached shipping rates
+	 *
+	 * @dataProvider provide_material_package_hash_fields
+	 *
+	 * @param callable $mutate_package Package mutation callback.
+	 */
+	public function test_calculate_shipping_for_package_invalidates_cache_for_material_package_changes( callable $mutate_package ) {
+		update_option( 'woocommerce_shipping_debug_mode', 'no' );
+		WC()->session->__unset( 'shipping_for_package_0' );
+
+		$filter_calls = 0;
+		$filter       = $this->get_package_rates_counter( $filter_calls );
+		$package      = $this->get_package_hash_test_package();
+
+		add_filter( 'woocommerce_package_rates', $filter, 10 );
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$mutate_package( $package );
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$this->assertSame( 2, $filter_calls );
+
+		remove_filter( 'woocommerce_package_rates', $filter, 10 );
+	}
+
+	/**
+	 * @testdox unknown package fields invalidate cached shipping rates by default
+	 */
+	public function test_calculate_shipping_for_package_invalidates_cache_for_unknown_package_fields_by_default() {
+		update_option( 'woocommerce_shipping_debug_mode', 'no' );
+		WC()->session->__unset( 'shipping_for_package_0' );
+
+		$filter_calls = 0;
+		$filter       = $this->get_package_rates_counter( $filter_calls );
+		$package      = $this->get_package_hash_test_package();
+
+		add_filter( 'woocommerce_package_rates', $filter, 10 );
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$package['custom_extension_key'] = 'changed';
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$this->assertSame( 2, $filter_calls );
+
+		remove_filter( 'woocommerce_package_rates', $filter, 10 );
+	}
+
+	/**
+	 * @testdox extensions can ignore package fields for the shipping-rate cache hash
+	 */
+	public function test_calculate_shipping_for_package_allows_extensions_to_ignore_package_hash_fields() {
+		update_option( 'woocommerce_shipping_debug_mode', 'no' );
+		WC()->session->__unset( 'shipping_for_package_0' );
+
+		$filter_calls          = 0;
+		$filter                = $this->get_package_rates_counter( $filter_calls );
+		$ignored_fields_filter = function ( array $ignored_fields ): array {
+			$ignored_fields[] = 'custom_extension_key';
+			return $ignored_fields;
+		};
+		$package               = $this->get_package_hash_test_package();
+
+		add_filter( 'woocommerce_package_rates', $filter, 10 );
+		add_filter( 'woocommerce_shipping_package_hash_ignored_fields', $ignored_fields_filter );
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$package['custom_extension_key'] = 'changed';
+
+		$this->sut->calculate_shipping_for_package( $package );
+
+		$this->assertSame( 1, $filter_calls );
+
+		remove_filter( 'woocommerce_shipping_package_hash_ignored_fields', $ignored_fields_filter );
+		remove_filter( 'woocommerce_package_rates', $filter, 10 );
+	}
+
 	/**
 	 * Data provider for test_package_rates_filter_error_handling.
 	 *
@@ -170,6 +281,92 @@ class WC_Shipping_Test extends WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * Data provider for ignored package hash fields.
+	 *
+	 * @return array[]
+	 */
+	public function provide_ignored_package_hash_fields(): array {
+		return array(
+			'subtotal'      => array( 'subtotal', 20 ),
+			'total'         => array( 'total', 20 ),
+			'package_id'    => array( 'package_id', 'package-1-changed' ),
+			'package_name'  => array( 'package_name', 'Package 1 Changed' ),
+			'rates'         => array( 'rates', array( 'prefilled_rate' => new WC_Shipping_Rate( 'prefilled_rate', 'Prefilled Rate', '7.00' ) ) ),
+			'package_index' => array( 'package_index', 2 ),
+		);
+	}
+
+	/**
+	 * Data provider for material package hash fields.
+	 *
+	 * @return array[]
+	 */
+	public function provide_material_package_hash_fields(): array {
+		return array(
+			'destination postcode' => array(
+				function ( array &$package ): void {
+					$package['destination']['postcode'] = '11111';
+				},
+			),
+			'contents cost'        => array(
+				function ( array &$package ): void {
+					$package['contents_cost'] = 20;
+				},
+			),
+			'cart contents'        => array(
+				function ( array &$package ): void {
+					$package['contents']['test_item']['quantity'] = 2;
+				},
+			),
+		);
+	}
+
+	/**
+	 * Get a package rates filter that counts recalculations.
+	 *
+	 * @param int $filter_calls Filter call count.
+	 * @return callable
+	 */
+	private function get_package_rates_counter( int &$filter_calls ): callable {
+		return function ( $rates ) use ( &$filter_calls ) {
+			++$filter_calls;
+			return $rates;
+		};
+	}
+
+	/**
+	 * Get a package for shipping hash tests.
+	 *
+	 * @return array
+	 */
+	private function get_package_hash_test_package(): array {
+		return array(
+			'contents'      => array(
+				'test_item' => array(
+					'quantity'          => 1,
+					'line_subtotal'     => 10,
+					'line_subtotal_tax' => 0,
+					'line_total'        => 10,
+					'line_tax'          => 0,
+					'data'              => new WC_Product_Simple(),
+				),
+			),
+			'contents_cost' => 10,
+			'destination'   => array(
+				'country'  => 'US',
+				'state'    => 'CA',
+				'postcode' => '00000',
+			),
+			'package_id'    => 'package-1',
+			'package_name'  => 'Package 1',
+			'package_index' => 1,
+			'subtotal'      => 10,
+			'total'         => 10,
+			'rates'         => array(),
+		);
+	}
+
 	/**
 	 * Data provider for test_calculate_shipping_for_hide_rates_when_free.
 	 *