Commit daae10db1a for woocommerce

commit daae10db1a9dcb58470ba0ff33b1927b5f21a9d8
Author: Mike Jolley <mike.jolley@me.com>
Date:   Tue Dec 16 13:59:03 2025 +0000

    Store API - Move `package_id` and `package_name` logic to core cart class (#62393)

    * Move package ID and Name creation to cart class

    * Use package name from package in cart template

    * Add test

    * Changelog

    * Inner package must be array

    * Remove get_package_name

    * Simplify package id indexing

diff --git a/plugins/woocommerce/changelog/wooplug-5959-storeapi-request-modifying-shipping-packages-might-cause b/plugins/woocommerce/changelog/wooplug-5959-storeapi-request-modifying-shipping-packages-might-cause
new file mode 100644
index 0000000000..1aa63ea766
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-5959-storeapi-request-modifying-shipping-packages-might-cause
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Moved package_id and package_name generation from CartController to WC_Cart::get_shipping_packages() method to ensure these fields are always present in shipping packages, regardless of how they are accessed.
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index 5381183765..c0d8659bd6 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -1579,7 +1579,14 @@ class WC_Cart extends WC_Legacy_Cart {
 	 * @return array of cart items
 	 */
 	public function get_shipping_packages() {
-		return apply_filters(
+		/**
+		 * Filters the shipping packages for the cart.
+		 *
+		 * @since 1.5.4
+		 * @param array $packages The shipping packages.
+		 * @return array The shipping packages.
+		 */
+		$shipping_packages = apply_filters(
 			'woocommerce_cart_shipping_packages',
 			array(
 				array(
@@ -1594,14 +1601,66 @@ class WC_Cart extends WC_Legacy_Cart {
 						'state'     => $this->get_customer()->get_shipping_state(),
 						'postcode'  => $this->get_customer()->get_shipping_postcode(),
 						'city'      => $this->get_customer()->get_shipping_city(),
-						'address'   => $this->get_customer()->get_shipping_address(),
-						'address_1' => $this->get_customer()->get_shipping_address(), // Provide both address and address_1 for backwards compatibility.
+						'address'   => $this->get_customer()->get_shipping_address(), // This is an alias of address_1, provided for backwards compatibility.
+						'address_1' => $this->get_customer()->get_shipping_address_1(),
 						'address_2' => $this->get_customer()->get_shipping_address_2(),
 					),
 					'cart_subtotal'   => $this->get_displayed_subtotal(),
 				),
 			)
 		);
+
+		// Return empty array if invalid object supplied by the filter or no packages.
+		if ( ! is_array( $shipping_packages ) || empty( $shipping_packages ) ) {
+			return array();
+		}
+
+		// Remove any invalid packages before adding package IDs.
+		$shipping_packages = array_filter(
+			$shipping_packages,
+			function ( $package ) {
+				return ! empty( $package ) && is_array( $package );
+			}
+		);
+
+		// Add package ID and package name to each package after the filter is applied.
+		$index = 1;
+		foreach ( $shipping_packages as $key => $package ) {
+			$shipping_packages[ $key ]['package_id']   = $package['package_id'] ?? $key;
+			$shipping_packages[ $key ]['package_name'] = $this->get_shipping_package_name( $shipping_packages[ $key ], $index );
+			++$index;
+		}
+
+		return $shipping_packages;
+	}
+
+	/**
+	 * Get the package name.
+	 *
+	 * @param array $package Shipping package data.
+	 * @param int   $index Package number.
+	 * @return string
+	 */
+	private function get_shipping_package_name( $package, $index ) {
+		/**
+		 * Filters the shipping package name.
+		 *
+		 * @since 4.3.0
+		 * @param string $shipping_package_name Shipping package name.
+		 * @param string $package_id Shipping package ID.
+		 * @param array $package Shipping package from WooCommerce.
+		 * @return string Shipping package name.
+		 */
+		return apply_filters(
+			'woocommerce_shipping_package_name',
+			sprintf(
+				/* translators: %d: shipping package number */
+				_x( 'Shipment %d', 'shipping packages', 'woocommerce' ),
+				$index
+			),
+			$package['package_id'],
+			$package
+		);
 	}

 	/**
diff --git a/plugins/woocommerce/includes/wc-cart-functions.php b/plugins/woocommerce/includes/wc-cart-functions.php
index ffa7feeb8c..f5081cc5e9 100644
--- a/plugins/woocommerce/includes/wc-cart-functions.php
+++ b/plugins/woocommerce/includes/wc-cart-functions.php
@@ -257,8 +257,7 @@ function wc_cart_totals_shipping_html() {
 				'show_package_details'     => count( $packages ) > 1,
 				'show_shipping_calculator' => is_cart() && apply_filters( 'woocommerce_shipping_show_shipping_calculator', $first, $i, $package ),
 				'package_details'          => implode( ', ', $product_names ),
-				/* translators: %d: shipping package number */
-				'package_name'             => apply_filters( 'woocommerce_shipping_package_name', ( ( $i + 1 ) > 1 ) ? sprintf( _x( 'Shipping %d', 'shipping packages', 'woocommerce' ), ( $i + 1 ) ) : _x( 'Shipping', 'shipping packages', 'woocommerce' ), $i, $package ),
+				'package_name'             => $package['package_name'],
 				'index'                    => $i,
 				'chosen_method'            => $chosen_method,
 				'formatted_destination'    => WC()->countries->get_formatted_address( $package['destination'], ', ' ),
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
index a076254d15..a2120758e8 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CartController.php
@@ -905,60 +905,14 @@ class CartController {

 		$packages = $cart->get_shipping_packages();

-		// Return early if invalid object supplied by the filter or no packages.
-		if ( ! is_array( $packages ) || empty( $packages ) ) {
+		// Return early if no packages.
+		if ( empty( $packages ) ) {
 			return [];
 		}

-		// Add extra package data to array.
-		$packages = array_map(
-			function ( $key, $package, $index ) {
-				$package['package_id']   = isset( $package['package_id'] ) ? $package['package_id'] : $key;
-				$package['package_name'] = isset( $package['package_name'] ) ? $package['package_name'] : $this->get_package_name( $package, $index );
-				return $package;
-			},
-			array_keys( $packages ),
-			$packages,
-			range( 1, count( $packages ) )
-		);
-
 		return $calculate_rates ? wc()->shipping()->calculate_shipping( $packages ) : $packages;
 	}

-	/**
-	 * Creates a name for a package.
-	 *
-	 * @param array $package Shipping package from WooCommerce.
-	 * @param int   $index Package number.
-	 * @return string
-	 */
-	protected function get_package_name( $package, $index ) {
-		/**
-		 * Filters the shipping package name.
-		 *
-		 * @since 4.3.0
-		 *
-		 * @internal Matches filter name in WooCommerce core.
-		 *
-		 * @param string $shipping_package_name Shipping package name.
-		 * @param string $package_id Shipping package ID.
-		 * @param array $package Shipping package from WooCommerce.
-		 * @return string Shipping package name.
-		 */
-		return apply_filters(
-			'woocommerce_shipping_package_name',
-			$index > 1 ?
-				sprintf(
-					/* translators: %d: shipping package number */
-					_x( 'Shipment %d', 'shipping packages', 'woocommerce' ),
-					$index
-				) :
-				_x( 'Shipment 1', 'shipping packages', 'woocommerce' ),
-			$package['package_id'],
-			$package
-		);
-	}
-
 	/**
 	 * Selects a shipping rate.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
index 580ead4fc2..729d6af767 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
@@ -18,6 +18,7 @@ class CartControllerTests extends TestCase {
 	public function tearDown(): void {
 		parent::tearDown();
 		WC()->cart->empty_cart();
+		remove_all_filters( 'woocommerce_cart_shipping_packages' );
 	}

 	/**
@@ -158,4 +159,128 @@ class CartControllerTests extends TestCase {
 			$this->assertContains( $expected_error, $error_codes );
 		}
 	}
+
+	/**
+	 * Test that get_shipping_packages returns packages with package_id and package_name.
+	 */
+	public function test_get_shipping_packages_includes_package_id_and_package_name() {
+		$class    = new CartController();
+		$fixtures = new FixtureData();
+		$fixtures->shipping_add_flat_rate();
+
+		$product = $fixtures->get_simple_product(
+			array(
+				'name'          => 'Test Product',
+				'regular_price' => 10,
+				'weight'        => 10,
+			)
+		);
+
+		wc()->cart->add_to_cart( $product->get_id(), 1 );
+
+		// Set shipping address so packages are generated.
+		wc()->customer->set_shipping_country( 'US' );
+		wc()->customer->set_shipping_state( 'CA' );
+		wc()->customer->set_shipping_postcode( '90210' );
+
+		$packages = $class->get_shipping_packages( false );
+
+		$this->assertNotEmpty( $packages, 'Should have at least one shipping package.' );
+		$this->assertArrayHasKey( 'package_id', $packages[0], 'Package should have package_id.' );
+		$this->assertArrayHasKey( 'package_name', $packages[0], 'Package should have package_name.' );
+		$this->assertEquals( 0, $packages[0]['package_id'], 'First package should have package_id of 0 (array key).' );
+		$this->assertStringContainsString( 'Shipment 1', $packages[0]['package_name'], 'First package should have package_name containing "Shipment 1".' );
+	}
+
+	/**
+	 * Test that get_shipping_packages handles multiple packages correctly.
+	 */
+	public function test_get_shipping_packages_handles_multiple_packages() {
+		$class    = new CartController();
+		$fixtures = new FixtureData();
+		$fixtures->shipping_add_flat_rate();
+
+		$product = $fixtures->get_simple_product(
+			array(
+				'name'          => 'Test Product',
+				'regular_price' => 10,
+				'weight'        => 10,
+			)
+		);
+
+		wc()->cart->add_to_cart( $product->get_id(), 1 );
+
+		// Set shipping address.
+		wc()->customer->set_shipping_country( 'US' );
+		wc()->customer->set_shipping_state( 'CA' );
+		wc()->customer->set_shipping_postcode( '90210' );
+
+		// Filter to create multiple packages.
+		add_filter(
+			'woocommerce_cart_shipping_packages',
+			function ( $packages ) {
+				$packages[] = $packages[0];
+				return $packages;
+			}
+		);
+
+		$packages = $class->get_shipping_packages( false );
+
+		$this->assertCount( 2, $packages, 'Should have two shipping packages.' );
+
+		// First package.
+		$this->assertArrayHasKey( 'package_id', $packages[0], 'First package should have package_id.' );
+		$this->assertArrayHasKey( 'package_name', $packages[0], 'First package should have package_name.' );
+		$this->assertEquals( 0, $packages[0]['package_id'], 'First package should have package_id of 0.' );
+		$this->assertStringContainsString( 'Shipment 1', $packages[0]['package_name'], 'First package should have package_name containing "Shipment 1".' );
+
+		// Second package.
+		$this->assertArrayHasKey( 'package_id', $packages[1], 'Second package should have package_id.' );
+		$this->assertArrayHasKey( 'package_name', $packages[1], 'Second package should have package_name.' );
+		$this->assertEquals( 1, $packages[1]['package_id'], 'Second package should have package_id of 1.' );
+		$this->assertStringContainsString( 'Shipment 2', $packages[1]['package_name'], 'Second package should have package_name containing "Shipment 2".' );
+
+		remove_all_filters( 'woocommerce_cart_shipping_packages' );
+	}
+
+	/**
+	 * Test that get_shipping_packages respects custom package_id from filter.
+	 */
+	public function test_get_shipping_packages_respects_custom_package_id() {
+		$class    = new CartController();
+		$fixtures = new FixtureData();
+		$fixtures->shipping_add_flat_rate();
+
+		$product = $fixtures->get_simple_product(
+			array(
+				'name'          => 'Test Product',
+				'regular_price' => 10,
+				'weight'        => 10,
+			)
+		);
+
+		wc()->cart->add_to_cart( $product->get_id(), 1 );
+
+		// Set shipping address.
+		wc()->customer->set_shipping_country( 'US' );
+		wc()->customer->set_shipping_state( 'CA' );
+		wc()->customer->set_shipping_postcode( '90210' );
+
+		// Filter to add custom package_id.
+		add_filter(
+			'woocommerce_cart_shipping_packages',
+			function ( $packages ) {
+				$packages[0]['package_id'] = 'custom-package-123';
+				return $packages;
+			}
+		);
+
+		$packages = $class->get_shipping_packages( false );
+
+		$this->assertNotEmpty( $packages, 'Should have at least one shipping package.' );
+		$this->assertEquals( 'custom-package-123', $packages[0]['package_id'], 'Package should use custom package_id from filter.' );
+		$this->assertArrayHasKey( 'package_name', $packages[0], 'Package should still have package_name.' );
+
+		remove_all_filters( 'woocommerce_cart_shipping_packages' );
+	}
 }