Commit 14c7c55c65e for woocommerce

commit 14c7c55c65e2c71dff9889eff0b854709599eb9a
Author: Mike Jolley <mike.jolley@me.com>
Date:   Thu Mar 19 11:27:35 2026 +0000

    Performance: Cache priming for HPOS orders (#63440)

    * Order Table Data Store - prime item caches

    * API v4 - Prime product caches

    * Prime order item meta data

    * changelog

    * Add targetHints for self to prevent extra permissions checks

    * Simplify target hint checks

    * Prime the needs_processing transient

    * return type

    * Add missing return type

    * Move shared priming methods to abstract

    * Fix type guard for post_type key

    * Simplify type guards

    * Update baseline

    * Guard against non-refund objects in prime_refund_caches_for_orders

    The woocommerce_hpos_pre_query filter can intercept nested
    wc_get_orders calls, returning non-refund objects even when
    type is shop_order_refund. Replace the type hint with an
    instanceof guard to handle this gracefully.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Reviewed code, small improvements

    * Simplify ID placeholders in queries

    * Fix phpstan warnings

    * Remove V4 controller changes split into PR #63654

    Removes targetHints on self links and product cache priming from the
    V4 Orders Controller — these are now in a separate PR for smaller
    review scope.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Update changelog to remove product cache reference

    Product cache priming was split into PR #63654.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Abstract refund total queries into shared base class

    Move get_total_tax_refunded, get_total_shipping_tax_refunded, and
    get_total_shipping_refunded from CPT and HPOS data stores into
    Abstract_WC_Order_Data_Store_CPT. The only difference between the
    two implementations was the refund orders JOIN clause, now provided
    by an overridable get_refund_orders_join_clause() method.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Abstract prime_refund_total_caches_for_orders into shared base class

    Move the refund total cache priming method from CPT and HPOS data
    stores into Abstract_WC_Order_Data_Store_CPT. The tax query uses
    overridable get_refund_orders_batch_join_clause() and
    get_refund_parent_column() methods. The fundamentally different
    refund totals query is handled by overridable get_batch_refund_totals().

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Add tests for HPOS order cache priming

    Tests that prime_caches_for_orders correctly populates refund total,
    tax refunded, and order item meta caches.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Unify refund total methods into shared get_refunded_item_meta_total

    Collapse get_total_tax_refunded, get_total_shipping_tax_refunded, and
    get_total_shipping_refunded into thin wrappers around a single
    get_refunded_item_meta_total() method parameterized by item type and
    meta keys.

    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

    * Address PR feedback: simplify array grouping and use prepared placeholders

    Replace array_reduce with plain foreach loops for meta data grouping in
    both abstract and CPT data stores. Use $wpdb->prepare with spread
    operator for meta key IN clauses instead of esc_sql interpolation.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Harden cache priming SQL and add CPT-path test coverage (#63705)

    - Replace esc_sql() with $wpdb->prepare() and %d placeholders for
      order item ID queries in prime_order_item_caches_for_orders
    - Change get_refund_orders_batch_join_clause() and get_batch_refund_totals()
      to accept array $order_ids instead of string $sanitized_ids, performing
      absint sanitization internally rather than trusting callers
    - Add CPT data store tests for prime_caches_for_orders covering both
      refund total cache priming and order item meta priming

    * Fix merge conflict

    ---------

    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
    Co-authored-by: Brandon Kraft <public@brandonkraft.com>

diff --git a/plugins/woocommerce/changelog/fix-hpos-order-item-cache-priming b/plugins/woocommerce/changelog/fix-hpos-order-item-cache-priming
new file mode 100644
index 00000000000..109694a7082
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-hpos-order-item-cache-priming
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Prime order item meta caches during HPOS order queries to eliminate N+1 queries in REST API serialization.
diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php
index 8ff22528977..f93fd6244b8 100644
--- a/plugins/woocommerce/includes/class-wc-order.php
+++ b/plugins/woocommerce/includes/class-wc-order.php
@@ -2192,7 +2192,7 @@ class WC_Order extends WC_Abstract_Order {
 	 *
 	 * Utilizes object cache to store refunds to avoid extra DB calls.
 	 *
-	 * @see WC_Order_Data_Store_CPT::prime_refund_caches_for_order()
+	 * @see Abstract_WC_Order_Data_Store_CPT::prime_refund_caches_for_orders()
 	 * @since 2.2
 	 *
 	 * @return WC_Order_Refund[]
diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
index 24fb26e2f22..5260753b308 100644
--- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
@@ -638,6 +638,117 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
 		}
 		$order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
 		update_meta_cache( 'order_item', $order_item_ids );
+
+		// Prime WC_Data meta cache (includes meta_id required by read_meta_data).
+		$id_placeholders     = implode( ', ', array_fill( 0, count( $order_item_ids ), '%d' ) );
+		$raw_meta_data_array = $wpdb->get_results(
+			$wpdb->prepare(
+				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $id_placeholders is generated above.
+				"SELECT order_item_id as object_id, meta_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE order_item_id IN ({$id_placeholders}) ORDER BY meta_id",
+				...$order_item_ids
+			)
+		);
+
+		if ( ! empty( $raw_meta_data_array ) ) {
+			$raw_meta_data_collection = array();
+			foreach ( $raw_meta_data_array as $raw_meta_data ) {
+				if ( ! isset( $raw_meta_data_collection[ $raw_meta_data->object_id ] ) ) {
+					$raw_meta_data_collection[ $raw_meta_data->object_id ] = array();
+				}
+				$raw_meta_data_collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
+			}
+			\WC_Order_Item::prime_raw_meta_data_cache( $raw_meta_data_collection, 'order-items' );
+		}
+	}
+
+	/**
+	 * Prime refund cache for a batch of orders.
+	 *
+	 * WC_Order::get_refunds() checks wp_cache before querying. By fetching
+	 * all refunds for the batch in a single query and populating the cache,
+	 * we eliminate one query per order.
+	 *
+	 * @param array $order_ids  Order IDs to prime cache for.
+	 * @param array $query_vars Query vars for the query.
+	 * @return void
+	 * @since 10.7.0
+	 */
+	protected function prime_refund_caches_for_orders( $order_ids, $query_vars ) {
+		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
+			if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'], true ) ) {
+				return;
+			}
+		}
+
+		$cache_keys_mapping = array();
+		foreach ( $order_ids as $order_id ) {
+			$cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id;
+		}
+
+		$non_cached_ids = array();
+		$cache_values   = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
+
+		if ( ! is_array( $cache_values ) ) {
+			$non_cached_ids = $order_ids;
+		} else {
+			foreach ( $order_ids as $order_id ) {
+				if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
+					$non_cached_ids[] = $order_id;
+				}
+			}
+		}
+
+		if ( empty( $non_cached_ids ) ) {
+			return;
+		}
+
+		/**
+		 * Fetch all refunds for the given order IDs.
+		 *
+		 * @var WC_Order_Refund[] $refunds
+		 */
+		$refunds = wc_get_orders(
+			array(
+				'type'            => 'shop_order_refund',
+				'post_parent__in' => $non_cached_ids,
+				'limit'           => -1,
+			)
+		);
+
+		$order_refunds = array();
+		foreach ( $refunds as $refund ) {
+			if ( $refund instanceof \WC_Order_Refund ) {
+				$order_refunds[ $refund->get_parent_id() ][] = $refund;
+			}
+		}
+
+		foreach ( $non_cached_ids as $order_id ) {
+			$cached_refunds = isset( $order_refunds[ $order_id ] ) ? $order_refunds[ $order_id ] : array();
+			wp_cache_set( $cache_keys_mapping[ $order_id ], $cached_refunds, 'orders' );
+		}
+	}
+
+	/**
+	 * Prime the needs_processing transient cache for a batch of orders.
+	 *
+	 * WC_Order::needs_processing() calls get_transient() per order, which
+	 * triggers an individual wp_options query each time. By priming the
+	 * object cache for all transient option names in a single query, we
+	 * eliminate the N+1.
+	 *
+	 * @param array $order_ids  Order IDs to prime cache for.
+	 * @param array $query_vars Query vars for the query.
+	 * @return void
+	 * @since 10.7.0
+	 */
+	protected function prime_needs_processing_transients( $order_ids, $query_vars ) {
+		$option_names = array();
+		foreach ( $order_ids as $order_id ) {
+			$option_names[] = '_transient_wc_order_' . $order_id . '_needs_processing';
+			$option_names[] = '_transient_timeout_wc_order_' . $order_id . '_needs_processing';
+		}
+
+		wp_prime_option_caches( $option_names );
 	}

 	/**
@@ -819,28 +930,225 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
 	}

 	/**
-	 * Get the total shipping tax refunded.
+	 * Returns a prepared SQL JOIN clause for finding refund orders belonging to a given parent order.
 	 *
-	 * @param  WC_Order $order Order object.
+	 * The clause aliases the refund table as `refunds`. Subclasses should override this
+	 * to use a different table (e.g. the HPOS orders table).
 	 *
-	 * @since 10.2.0
-	 * @return float
+	 * @since 10.7.0
+	 * @param int $order_id Parent order ID.
+	 * @return string Prepared SQL JOIN fragment.
 	 */
-	public function get_total_shipping_tax_refunded( $order ) {
+	protected function get_refund_orders_join_clause( int $order_id ): string {
 		global $wpdb;
+		return $wpdb->prepare( '%i AS refunds ON ( refunds.post_type = %s AND refunds.post_parent = %d )', $wpdb->posts, 'shop_order_refund', $order_id );
+	}
+
+	/**
+	 * Returns a prepared SQL JOIN clause for finding refund orders belonging to multiple parent orders.
+	 *
+	 * The clause aliases the refund table as `refunds`. Subclasses should override this
+	 * to use a different table (e.g. the HPOS orders table).
+	 *
+	 * @since 10.7.0
+	 * @param array $order_ids List of order IDs.
+	 * @return string Prepared SQL JOIN fragment.
+	 */
+	protected function get_refund_orders_batch_join_clause( array $order_ids ): string {
+		global $wpdb;
+		$id_list = implode( ', ', array_map( 'absint', $order_ids ) );
+		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $id_list is sanitized via absint above.
+		return $wpdb->prepare( "%i AS refunds ON ( refunds.post_type = %s AND refunds.post_parent IN ( $id_list ) )", $wpdb->posts, 'shop_order_refund' );
+	}
+
+	/**
+	 * Returns the column name on the refund table alias (`refunds`) that holds the parent order ID.
+	 *
+	 * @since 10.7.0
+	 * @return string Column reference, e.g. 'refunds.post_parent'.
+	 */
+	protected function get_refund_parent_column(): string {
+		return 'refunds.post_parent';
+	}
+
+	/**
+	 * Query total refunded amounts per order in a batch. Returns an associative array
+	 * of order_id => total (positive float).
+	 *
+	 * Subclasses should override this when the refund total is stored differently
+	 * (e.g. HPOS stores it directly in the orders table rather than postmeta).
+	 *
+	 * @since 10.7.0
+	 * @param array $order_ids List of order IDs.
+	 * @return array<int, float> Map of order_id => refund total.
+	 */
+	protected function get_batch_refund_totals( array $order_ids ): array {
+		global $wpdb;
+
+		$id_list = implode( ', ', array_map( 'absint', $order_ids ) );
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $id_list is sanitized via absint above.
+		$refund_totals = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT posts.post_parent AS order_id, SUM( postmeta.meta_value ) AS total
+				FROM %i AS postmeta
+				INNER JOIN %i AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent IN ( $id_list ) )
+				WHERE postmeta.meta_key = '_refund_amount'
+				AND postmeta.post_id = posts.ID
+				GROUP BY posts.post_parent",
+				$wpdb->postmeta,
+				$wpdb->posts
+			)
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+		$totals_by_order = array();
+		foreach ( $refund_totals as $row ) {
+			$totals_by_order[ $row->order_id ] = floatval( $row->total );
+		}
+
+		return $totals_by_order;
+	}
+
+	/**
+	 * Get the summed refund item meta value for a given order, item type, and meta keys.
+	 *
+	 * @since 10.7.0
+	 * @param WC_Order $order     Order object.
+	 * @param string   $item_type Order item type (e.g. 'tax', 'shipping').
+	 * @param array    $meta_keys Meta keys to sum.
+	 * @return float Absolute total.
+	 */
+	protected function get_refunded_item_meta_total( $order, string $item_type, array $meta_keys ): float {
+		global $wpdb;
+
+		$refund_join      = $this->get_refund_orders_join_clause( $order->get_id() );
+		$meta_placeholder = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) );

 		$total = $wpdb->get_var(
+			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $refund_join is already prepared.
+			// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $meta_keys is splatted.
 			$wpdb->prepare(
 				"SELECT SUM( order_itemmeta.meta_value )
-				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
-				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
-				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' )
+				FROM %i AS order_itemmeta
+				INNER JOIN $refund_join
+				INNER JOIN %i AS order_items ON ( order_items.order_id = refunds.id AND order_items.order_item_type = %s )
 				WHERE order_itemmeta.order_item_id = order_items.order_item_id
-				AND order_itemmeta.meta_key = 'shipping_tax_amount'",
-				$order->get_id()
+				AND order_itemmeta.meta_key IN ( $meta_placeholder )",
+				$wpdb->prefix . 'woocommerce_order_itemmeta',
+				$wpdb->prefix . 'woocommerce_order_items',
+				$item_type,
+				...$meta_keys,
 			)
+			// phpcs:enable
 		) ?? 0;

 		return abs( $total );
 	}
+
+	/**
+	 * Get the total tax refunded.
+	 *
+	 * @param WC_Order $order Order object.
+	 * @return float
+	 */
+	public function get_total_tax_refunded( $order ) {
+		return $this->get_refunded_item_meta_total( $order, 'tax', array( 'tax_amount', 'shipping_tax_amount' ) );
+	}
+
+	/**
+	 * Get the total shipping tax refunded.
+	 *
+	 * @param WC_Order $order Order object.
+	 *
+	 * @since 10.2.0
+	 * @return float
+	 */
+	public function get_total_shipping_tax_refunded( $order ) {
+		return $this->get_refunded_item_meta_total( $order, 'tax', array( 'shipping_tax_amount' ) );
+	}
+
+	/**
+	 * Get the total shipping refunded.
+	 *
+	 * @param WC_Order $order Order object.
+	 * @return float
+	 */
+	public function get_total_shipping_refunded( $order ) {
+		return $this->get_refunded_item_meta_total( $order, 'shipping', array( 'cost' ) );
+	}
+
+	/**
+	 * Prime the refund total and refund tax total caches for a batch of orders.
+	 *
+	 * @since 10.7.0
+	 * @param array $order_ids  Order IDs to prime cache for.
+	 * @param array $query_vars Query vars for the query.
+	 * @return void
+	 */
+	protected function prime_refund_total_caches_for_orders( $order_ids, $query_vars ): void {
+		global $wpdb;
+
+		$cache_prefix = \WC_Cache_Helper::get_cache_prefix( 'orders' );
+
+		// Find which orders need priming (check both total_refunded and total_tax_refunded).
+		$total_keys     = array();
+		$tax_keys       = array();
+		$non_cached_ids = array();
+		foreach ( $order_ids as $order_id ) {
+			$total_keys[ $order_id ] = $cache_prefix . 'total_refunded' . $order_id;
+			$tax_keys[ $order_id ]   = $cache_prefix . 'total_tax_refunded' . $order_id;
+		}
+
+		$all_keys     = array_merge( array_values( $total_keys ), array_values( $tax_keys ) );
+		$cache_values = wc_cache_get_multiple( $all_keys, 'orders' );
+
+		if ( ! is_array( $cache_values ) ) {
+			$non_cached_ids = $order_ids;
+		} else {
+			foreach ( $order_ids as $order_id ) {
+				if ( false === $cache_values[ $total_keys[ $order_id ] ] || false === $cache_values[ $tax_keys[ $order_id ] ] ) {
+					$non_cached_ids[] = $order_id;
+				}
+			}
+		}
+
+		if ( empty( $non_cached_ids ) ) {
+			return;
+		}
+
+		// Batch query: total refunded per order.
+		$totals_by_order = $this->get_batch_refund_totals( $non_cached_ids );
+		foreach ( $non_cached_ids as $order_id ) {
+			wp_cache_set( $total_keys[ $order_id ], $totals_by_order[ $order_id ] ?? 0.0, 'orders' );
+		}
+
+		// Batch query: total tax refunded per order.
+		$refund_join = $this->get_refund_orders_batch_join_clause( $non_cached_ids );
+		$parent_col  = $this->get_refund_parent_column();
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $refund_join is already prepared, $parent_col is hardcoded.
+		$tax_totals = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT $parent_col AS order_id, SUM( order_itemmeta.meta_value ) AS total
+				FROM %i AS order_itemmeta
+				INNER JOIN $refund_join
+				INNER JOIN %i AS order_items ON ( order_items.order_id = refunds.id AND order_items.order_item_type = 'tax' )
+				WHERE order_itemmeta.order_item_id = order_items.order_item_id
+				AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')
+				GROUP BY $parent_col",
+				$wpdb->prefix . 'woocommerce_order_itemmeta',
+				$wpdb->prefix . 'woocommerce_order_items'
+			)
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+		$tax_by_order = array();
+		foreach ( $tax_totals as $row ) {
+			$tax_by_order[ $row->order_id ] = abs( floatval( $row->total ) );
+		}
+		foreach ( $non_cached_ids as $order_id ) {
+			wp_cache_set( $tax_keys[ $order_id ], $tax_by_order[ $order_id ] ?? 0.0, 'orders' );
+		}
+	}
 }
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
index dcec945fb2e..3060c096854 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
@@ -453,54 +453,6 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
 		return floatval( $total );
 	}

-	/**
-	 * Get the total tax refunded.
-	 *
-	 * @param  WC_Order $order Order object.
-	 * @return float
-	 */
-	public function get_total_tax_refunded( $order ) {
-		global $wpdb;
-
-		$total = $wpdb->get_var(
-			$wpdb->prepare(
-				"SELECT SUM( order_itemmeta.meta_value )
-				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
-				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
-				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' )
-				WHERE order_itemmeta.order_item_id = order_items.order_item_id
-				AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
-				$order->get_id()
-			)
-		) ?? 0;
-
-		return abs( $total );
-	}
-
-	/**
-	 * Get the total shipping refunded.
-	 *
-	 * @param  WC_Order $order Order object.
-	 * @return float
-	 */
-	public function get_total_shipping_refunded( $order ) {
-		global $wpdb;
-
-		$total = $wpdb->get_var(
-			$wpdb->prepare(
-				"SELECT SUM( order_itemmeta.meta_value )
-				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
-				INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d )
-				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' )
-				WHERE order_itemmeta.order_item_id = order_items.order_item_id
-				AND order_itemmeta.meta_key IN ('cost')",
-				$order->get_id()
-			)
-		) ?? 0;
-
-		return abs( $total );
-	}
-
 	/**
 	 * Finds an Order ID based on an order key.
 	 *
@@ -1214,69 +1166,22 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
 	 *
 	 * @param array $order_ids List of order IDS to prime caches for.
 	 * @param array $query_vars Original query arguments.
+	 * @return void
 	 */
 	public function prime_caches_for_orders( $order_ids, $query_vars ) {
-		// Lets do some cache hydrations so that we don't have to fetch data from DB for every order.
 		$this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars );
-		$this->prime_refund_caches_for_order( $order_ids, $query_vars );
 		$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );
-	}

-	/**
-	 * Prime refund cache for orders.
-	 *
-	 * @param array $order_ids  Order Ids to prime cache for.
-	 * @param array $query_vars Query vars for the query.
-	 */
-	private function prime_refund_caches_for_order( $order_ids, $query_vars ) {
-		if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) {
-			return;
-		}
-		if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
-			if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'], true ) ) {
-				return;
-			}
-		}
-		$cache_keys_mapping = array();
-		foreach ( $order_ids as $order_id ) {
-			$cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id;
-		}
-		$non_cached_ids = array();
-		$cache_values   = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
-		foreach ( $order_ids as $order_id ) {
-			if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
-				$non_cached_ids[] = $order_id;
-			}
-		}
-		if ( empty( $non_cached_ids ) ) {
+		// The following priming methods only apply to shop_order queries.
+		$order_type = $query_vars['type'] ?? $query_vars['post_type'] ?? '';
+		$order_type = is_array( $order_type ) ? $order_type : array( $order_type );
+		if ( ! in_array( 'shop_order', $order_type, true ) ) {
 			return;
 		}

-		$refunds       = wc_get_orders(
-			array(
-				'type'            => 'shop_order_refund',
-				'post_parent__in' => $non_cached_ids,
-				'limit'           => - 1,
-			)
-		);
-		$order_refunds = array_reduce(
-			$refunds,
-			function ( $order_refunds_array, WC_Order_Refund $refund ) {
-				if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) {
-					$order_refunds_array[ $refund->get_parent_id() ] = array();
-				}
-				$order_refunds_array[ $refund->get_parent_id() ][] = $refund;
-				return $order_refunds_array;
-			},
-			array()
-		);
-		foreach ( $non_cached_ids as $order_id ) {
-			$refunds = array();
-			if ( isset( $order_refunds[ $order_id ] ) ) {
-				$refunds = $order_refunds[ $order_id ];
-			}
-			wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' );
-		}
+		$this->prime_refund_caches_for_orders( $order_ids, $query_vars );
+		$this->prime_refund_total_caches_for_orders( $order_ids, $query_vars );
+		$this->prime_needs_processing_transients( $order_ids, $query_vars );
 	}

 	/**
@@ -1318,17 +1223,13 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
 				ORDER BY post_id"
 		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 		);
-		$raw_meta_data_collection = array_reduce(
-			$raw_meta_data_array,
-			function ( $collection, $raw_meta_data ) {
-				if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) {
-					$collection[ $raw_meta_data->object_id ] = array();
-				}
-				$collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
-				return $collection;
-			},
-			array()
-		);
+		$raw_meta_data_collection = array();
+		foreach ( $raw_meta_data_array as $raw_meta_data ) {
+			if ( ! isset( $raw_meta_data_collection[ $raw_meta_data->object_id ] ) ) {
+				$raw_meta_data_collection[ $raw_meta_data->object_id ] = array();
+			}
+			$raw_meta_data_collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
+		}
 		WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
 	}

diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index eaa61ed32f1..047532137dc 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -17706,12 +17706,6 @@ parameters:
 			count: 1
 			path: includes/data-stores/class-wc-order-data-store-cpt.php

-		-
-			message: '#^Cannot access offset non\-falsy\-string on array\|bool\.$#'
-			identifier: offsetAccess.nonOffsetAccessible
-			count: 1
-			path: includes/data-stores/class-wc-order-data-store-cpt.php
-
 		-
 			message: '#^Cannot access offset string on array\|bool\.$#'
 			identifier: offsetAccess.nonOffsetAccessible
@@ -17760,24 +17754,12 @@ parameters:
 			count: 1
 			path: includes/data-stores/class-wc-order-data-store-cpt.php

-		-
-			message: '#^Method WC_Order_Data_Store_CPT\:\:prime_caches_for_orders\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: includes/data-stores/class-wc-order-data-store-cpt.php
-
 		-
 			message: '#^Method WC_Order_Data_Store_CPT\:\:prime_raw_meta_cache_for_orders\(\) has no return type specified\.$#'
 			identifier: missingType.return
 			count: 1
 			path: includes/data-stores/class-wc-order-data-store-cpt.php

-		-
-			message: '#^Method WC_Order_Data_Store_CPT\:\:prime_refund_caches_for_order\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: includes/data-stores/class-wc-order-data-store-cpt.php
-
 		-
 			message: '#^Method WC_Order_Data_Store_CPT\:\:read_cogs_data\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -17856,12 +17838,6 @@ parameters:
 			count: 1
 			path: includes/data-stores/class-wc-order-data-store-cpt.php

-		-
-			message: '#^Parameter \#1 \$input of function array_reduce expects array\<WC_Order\>, array\<WC_Order\>\|stdClass given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/data-stores/class-wc-order-data-store-cpt.php
-
 		-
 			message: '#^Parameter \#1 \$post_id of function get_post_meta expects int, bool\|int given\.$#'
 			identifier: argument.type
@@ -17892,12 +17868,6 @@ parameters:
 			count: 1
 			path: includes/data-stores/class-wc-order-data-store-cpt.php

-		-
-			message: '#^Parameter \#2 \$callback of function array_reduce expects callable\(mixed, WC_Order\)\: mixed, Closure\(mixed, WC_Order_Refund\)\: mixed given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/data-stores/class-wc-order-data-store-cpt.php
-
 		-
 			message: '#^Parameter \#3 \$index_key of function wp_list_pluck expects int\|string\|null, true given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index f8bf781377f..5da09df805d 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -1039,90 +1039,17 @@ WHERE
 	}

 	/**
-	 * Get the total tax refunded.
+	 * Returns a prepared SQL JOIN clause for finding refund orders belonging to a given parent order.
 	 *
-	 * @param WC_Order $order Order object.
-	 *
-	 * @return float
-	 */
-	public function get_total_tax_refunded( $order ) {
-		global $wpdb;
-
-		$order_table = self::get_orders_table_name();
-
-		$total = $wpdb->get_var(
-			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
-			$wpdb->prepare(
-				"SELECT SUM( order_itemmeta.meta_value )
-				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
-				INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
-				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'tax' )
-				WHERE order_itemmeta.order_item_id = order_items.order_item_id
-				AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
-				$order->get_id(),
-			)
-		) ?? 0;
-		// phpcs:enable
-
-		return abs( $total );
-	}
-
-	/**
-	 * Get the total shipping tax refunded.
+	 * Overrides the CPT version to use the HPOS orders table.
 	 *
-	 * @param WC_Order $order Order object.
-	 *
-	 * @since 10.2.0
-	 * @return float
+	 * @since 10.7.0
+	 * @param int $order_id Parent order ID.
+	 * @return string Prepared SQL JOIN fragment.
 	 */
-	public function get_total_shipping_tax_refunded( $order ) {
+	protected function get_refund_orders_join_clause( int $order_id ): string {
 		global $wpdb;
-
-		$order_table = self::get_orders_table_name();
-
-		$total = $wpdb->get_var(
-			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
-			$wpdb->prepare(
-				"SELECT SUM( order_itemmeta.meta_value )
-				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
-				INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
-				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'tax' )
-				WHERE order_itemmeta.order_item_id = order_items.order_item_id
-				AND order_itemmeta.meta_key = 'shipping_tax_amount'",
-				$order->get_id()
-			)
-		) ?? 0;
-		// phpcs:enable
-
-		return abs( $total );
-	}
-
-	/**
-	 * Get the total shipping refunded.
-	 *
-	 * @param  WC_Order $order Order object.
-	 * @return float
-	 */
-	public function get_total_shipping_refunded( $order ) {
-		global $wpdb;
-
-		$order_table = self::get_orders_table_name();
-
-		$total = $wpdb->get_var(
-			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
-			$wpdb->prepare(
-				"SELECT SUM( order_itemmeta.meta_value )
-				FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta
-				INNER JOIN $order_table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
-				INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'shipping' )
-				WHERE order_itemmeta.order_item_id = order_items.order_item_id
-				AND order_itemmeta.meta_key IN ('cost')",
-				$order->get_id()
-			)
-		) ?? 0;
-		// phpcs:enable
-
-		return abs( $total );
+		return $wpdb->prepare( '%i AS refunds ON ( refunds.type = %s AND refunds.parent_order_id = %d )', self::get_orders_table_name(), 'shop_order_refund', $order_id );
 	}

 	/**
@@ -3188,6 +3115,7 @@ FROM $order_meta_table
 			$orders = $query->orders;
 		} else {
 			$orders = WC()->order_factory->get_orders( $query->orders );
+			$this->prime_caches_for_orders( $query->orders, $query_vars );
 		}

 		if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
@@ -3203,6 +3131,89 @@ FROM $order_meta_table

 	//phpcs:enable Squiz.Commenting, Generic.Commenting

+	/**
+	 * Prime caches for a collection of orders. Reduces N+1 queries when iterating over order results.
+	 *
+	 * @param array $order_ids  List of order IDs to prime caches for.
+	 * @param array $query_vars Original query arguments.
+	 * @return void
+	 */
+	public function prime_caches_for_orders( array $order_ids, array $query_vars ): void {
+		$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );
+
+		// The following priming methods only apply to shop_order queries.
+		$order_type = $query_vars['type'] ?? $query_vars['post_type'] ?? '';
+		$order_type = is_array( $order_type ) ? $order_type : array( $order_type );
+		if ( ! in_array( 'shop_order', $order_type, true ) ) {
+			return;
+		}
+
+		$this->prime_refund_caches_for_orders( $order_ids, $query_vars );
+		$this->prime_refund_total_caches_for_orders( $order_ids, $query_vars );
+		$this->prime_needs_processing_transients( $order_ids, $query_vars );
+	}
+
+	/**
+	 * Returns a prepared SQL JOIN clause for finding refund orders belonging to multiple parent orders.
+	 *
+	 * Overrides the CPT version to use the HPOS orders table.
+	 *
+	 * @since 10.7.0
+	 * @param array $order_ids List of order IDs.
+	 * @return string Prepared SQL JOIN fragment.
+	 */
+	protected function get_refund_orders_batch_join_clause( array $order_ids ): string {
+		global $wpdb;
+		$id_list = implode( ', ', array_map( 'absint', $order_ids ) );
+		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $id_list is sanitized via absint above.
+		return $wpdb->prepare( "%i AS refunds ON ( refunds.type = %s AND refunds.parent_order_id IN ( $id_list ) )", self::get_orders_table_name(), 'shop_order_refund' );
+	}
+
+	/**
+	 * Returns the column name on the refund table alias (`refunds`) that holds the parent order ID.
+	 *
+	 * @since 10.7.0
+	 * @return string Column reference.
+	 */
+	protected function get_refund_parent_column(): string {
+		return 'refunds.parent_order_id';
+	}
+
+	/**
+	 * Query total refunded amounts per order in a batch.
+	 *
+	 * Overrides the CPT version to read directly from the HPOS orders table
+	 * rather than joining postmeta.
+	 *
+	 * @since 10.7.0
+	 * @param array $order_ids List of order IDs.
+	 * @return array<int, float> Map of order_id => refund total.
+	 */
+	protected function get_batch_refund_totals( array $order_ids ): array {
+		global $wpdb;
+
+		$id_list = implode( ', ', array_map( 'absint', $order_ids ) );
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $id_list is sanitized via absint above.
+		$refund_totals = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT parent_order_id AS order_id, SUM( total_amount ) AS total
+				FROM %i
+				WHERE type = 'shop_order_refund' AND parent_order_id IN ( $id_list )
+				GROUP BY parent_order_id",
+				self::get_orders_table_name()
+			)
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+		$totals_by_order = array();
+		foreach ( $refund_totals as $row ) {
+			$totals_by_order[ $row->order_id ] = -1 * floatval( $row->total );
+		}
+
+		return $totals_by_order;
+	}
+
 	/**
 	 * Get the SQL needed to create all the tables needed for the custom orders table feature.
 	 *
diff --git a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Orders/Controller.php b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Orders/Controller.php
index 127fa72ab91..e05b1503d89 100644
--- a/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Orders/Controller.php
+++ b/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Orders/Controller.php
@@ -319,7 +319,6 @@ class Controller extends AbstractController {
 		}
 		$product_ids = array_unique( array_filter( $product_ids ) );
 		if ( ! empty( $product_ids ) ) {
-			// Prime caches to reduce future queries.
 			_prime_post_caches( $product_ids, true, true );
 		}

diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
index ac1b01c693c..cbbea52799a 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-order-data-store-cpt-test.php
@@ -1257,4 +1257,112 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
 		$expected_final_cogs = $expected_order_cogs + $expected_refund_cogs;
 		$this->assertEquals( $expected_final_cogs, $order->get_cogs_total_value(), 'Order COGS should be reduced by refund amount' );
 	}
+
+	/**
+	 * @testdox CPT cache priming populates refund total and tax caches with correct values.
+	 */
+	public function test_prime_caches_for_orders_primes_refund_totals(): void {
+		update_option( 'woocommerce_prices_include_tax', 'yes' );
+		update_option( 'woocommerce_calc_taxes', 'yes' );
+
+		WC_Tax::_insert_tax_rate(
+			array(
+				'tax_rate_country'  => '',
+				'tax_rate'          => '20',
+				'tax_rate_name'     => 'tax',
+				'tax_rate_order'    => '1',
+				'tax_rate_shipping' => '1',
+			)
+		);
+
+		$rate = new WC_Shipping_Rate( 'flat_rate_shipping', 'Flat rate shipping', '10', array(), 'flat_rate' );
+		$item = new WC_Order_Item_Shipping();
+		$item->set_props(
+			array(
+				'method_title' => $rate->label,
+				'method_id'    => $rate->id,
+				'total'        => wc_format_decimal( $rate->cost ),
+				'taxes'        => $rate->taxes,
+			)
+		);
+
+		$order = WC_Helper_Order::create_order();
+		$order->add_item( $item );
+		$order->calculate_totals();
+		$order->save();
+
+		$product_item_id  = current( $order->get_items() )->get_id();
+		$shipping_item_id = current( $order->get_items( 'shipping' ) )->get_id();
+
+		wc_create_refund(
+			array(
+				'order_id'   => $order->get_id(),
+				'line_items' => array(
+					$product_item_id  => array(
+						'id'           => $product_item_id,
+						'qty'          => 1,
+						'refund_total' => 10,
+						'refund_tax'   => array( 1 => 2 ),
+					),
+					$shipping_item_id => array(
+						'id'           => $shipping_item_id,
+						'qty'          => 1,
+						'refund_total' => 10,
+						'refund_tax'   => array( 1 => 3 ),
+					),
+				),
+			)
+		);
+
+		wp_cache_flush();
+		WC_Cache_Helper::invalidate_cache_group( 'orders' );
+
+		$data_store = WC_Data_Store::load( 'order' );
+		$data_store->prime_caches_for_orders(
+			array( $order->get_id() ),
+			array(
+				'fields'    => 'all',
+				'post_type' => 'shop_order',
+			)
+		);
+
+		$cache_prefix = WC_Cache_Helper::get_cache_prefix( 'orders' );
+		$order_id     = $order->get_id();
+
+		$cached_total_refunded = wp_cache_get( $cache_prefix . 'total_refunded' . $order_id, 'orders' );
+		$cached_tax_refunded   = wp_cache_get( $cache_prefix . 'total_tax_refunded' . $order_id, 'orders' );
+
+		$this->assertNotFalse( $cached_total_refunded, 'Total refunded should be cached after priming' );
+		$this->assertNotFalse( $cached_tax_refunded, 'Total tax refunded should be cached after priming' );
+		$this->assertIsFloat( $cached_total_refunded, 'Cached total refunded should be a float' );
+		$this->assertEquals( 5.0, $cached_tax_refunded, 'Cached tax refunded should equal sum of product tax (2) + shipping tax (3)' );
+	}
+
+	/**
+	 * @testdox CPT cache priming populates order item meta caches so item access does not trigger additional queries.
+	 */
+	public function test_prime_caches_for_orders_primes_item_meta(): void {
+		$order = WC_Helper_Order::create_order();
+
+		wp_cache_flush();
+		WC_Cache_Helper::invalidate_cache_group( 'orders' );
+
+		$data_store = WC_Data_Store::load( 'order' );
+		$data_store->prime_caches_for_orders(
+			array( $order->get_id() ),
+			array(
+				'fields'    => 'all',
+				'post_type' => 'shop_order',
+			)
+		);
+
+		$reloaded_order = wc_get_order( $order->get_id() );
+		$items          = $reloaded_order->get_items();
+
+		$this->assertNotEmpty( $items, 'Order should have line items' );
+
+		foreach ( $items as $item ) {
+			$this->assertGreaterThan( 0, $item->get_product_id(), 'Item should have a product ID from cached meta' );
+		}
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreCachePrimingTest.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreCachePrimingTest.php
new file mode 100644
index 00000000000..660502f2eed
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreCachePrimingTest.php
@@ -0,0 +1,178 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\DataStores\Orders;
+
+use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
+use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
+use Automattic\WooCommerce\Utilities\OrderUtil;
+use WC_Helper_Product;
+use WC_Order;
+use WC_Order_Item_Shipping;
+use WC_Shipping_Rate;
+use WC_Tax;
+
+/**
+ * Tests for cache priming in OrdersTableDataStore.
+ */
+class OrdersTableDataStoreCachePrimingTest extends \HposTestCase {
+	use HPOSToggleTrait;
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var OrdersTableDataStore
+	 */
+	private $sut;
+
+	/**
+	 * Whether COT was enabled before the test.
+	 *
+	 * @var bool
+	 */
+	private $cot_state;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		add_filter( 'wc_allow_changing_orders_storage_while_sync_is_pending', '__return_true' );
+
+		$this->setup_cot();
+		$this->cot_state = OrderUtil::custom_orders_table_usage_is_enabled();
+		$this->toggle_cot_feature_and_usage( true );
+
+		$container = wc_get_container();
+		$container->reset_all_resolved();
+		$this->sut = $container->get( OrdersTableDataStore::class );
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		$this->toggle_cot_feature_and_usage( $this->cot_state );
+		$this->clean_up_cot_setup();
+
+		remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Cache priming populates refund total and tax caches with correct values.
+	 */
+	public function test_prime_caches_for_orders_primes_refund_totals(): void {
+		update_option( 'woocommerce_prices_include_tax', 'yes' );
+		update_option( 'woocommerce_calc_taxes', 'yes' );
+
+		WC_Tax::_insert_tax_rate(
+			array(
+				'tax_rate_country'  => '',
+				'tax_rate'          => '20',
+				'tax_rate_name'     => 'tax',
+				'tax_rate_order'    => '1',
+				'tax_rate_shipping' => '1',
+			)
+		);
+
+		$rate = new WC_Shipping_Rate( 'flat_rate_shipping', 'Flat rate shipping', '10', array(), 'flat_rate' );
+		$item = new WC_Order_Item_Shipping();
+		$item->set_props(
+			array(
+				'method_title' => $rate->label,
+				'method_id'    => $rate->id,
+				'total'        => wc_format_decimal( $rate->cost ),
+				'taxes'        => $rate->taxes,
+			)
+		);
+
+		$order = new WC_Order();
+		$order->save();
+		$order->add_product( WC_Helper_Product::create_simple_product(), 10 );
+		$order->add_item( $item );
+		$order->calculate_totals();
+		$order->save();
+
+		$product_item_id  = current( $order->get_items() )->get_id();
+		$shipping_item_id = current( $order->get_items( 'shipping' ) )->get_id();
+
+		wc_create_refund(
+			array(
+				'order_id'   => $order->get_id(),
+				'line_items' => array(
+					$product_item_id  => array(
+						'id'           => $product_item_id,
+						'qty'          => 1,
+						'refund_total' => 10,
+						'refund_tax'   => array( 1 => 2 ),
+					),
+					$shipping_item_id => array(
+						'id'           => $shipping_item_id,
+						'qty'          => 1,
+						'refund_total' => 10,
+						'refund_tax'   => array( 1 => 3 ),
+					),
+				),
+			)
+		);
+
+		// Clear all caches so prime_caches_for_orders has to do real work.
+		wp_cache_flush();
+		\WC_Cache_Helper::invalidate_cache_group( 'orders' );
+
+		$this->sut->prime_caches_for_orders(
+			array( $order->get_id() ),
+			array(
+				'fields' => 'all',
+				'type'   => 'shop_order',
+			)
+		);
+
+		// Verify refund total caches were primed with correct values.
+		$cache_prefix = \WC_Cache_Helper::get_cache_prefix( 'orders' );
+		$order_id     = $order->get_id();
+
+		$cached_total_refunded = wp_cache_get( $cache_prefix . 'total_refunded' . $order_id, 'orders' );
+		$cached_tax_refunded   = wp_cache_get( $cache_prefix . 'total_tax_refunded' . $order_id, 'orders' );
+
+		$this->assertNotFalse( $cached_total_refunded, 'Total refunded should be cached after priming' );
+		$this->assertNotFalse( $cached_tax_refunded, 'Total tax refunded should be cached after priming' );
+		$this->assertIsFloat( $cached_total_refunded, 'Cached total refunded should be a float' );
+		$this->assertEquals( 5.0, $cached_tax_refunded, 'Cached tax refunded should equal sum of product tax (2) + shipping tax (3)' );
+	}
+
+	/**
+	 * @testdox Cache priming populates order item meta caches so item access does not trigger additional queries.
+	 */
+	public function test_prime_caches_for_orders_primes_item_meta(): void {
+		$order = new WC_Order();
+		$order->save();
+		$order->add_product( WC_Helper_Product::create_simple_product(), 2 );
+		$order->calculate_totals();
+		$order->save();
+
+		// Clear all caches.
+		wp_cache_flush();
+		\WC_Cache_Helper::invalidate_cache_group( 'orders' );
+
+		$this->sut->prime_caches_for_orders(
+			array( $order->get_id() ),
+			array(
+				'fields' => 'all',
+				'type'   => 'shop_order',
+			)
+		);
+
+		// Reload order and access items — meta should already be cached.
+		$reloaded_order = wc_get_order( $order->get_id() );
+		$items          = $reloaded_order->get_items();
+
+		$this->assertNotEmpty( $items, 'Order should have line items' );
+
+		foreach ( $items as $item ) {
+			$this->assertGreaterThan( 0, $item->get_product_id(), 'Item should have a product ID from cached meta' );
+		}
+	}
+}