Commit efb47b70bc8 for woocommerce

commit efb47b70bc8c34caae53d717ffb2ff11218b3dd4
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Wed Apr 29 13:18:11 2026 +0200

    [Performance] Reduce the number of order saves during checkout in StoreAPI (#64392)

    Reduces the number of order/customer save-calls, wc_get_price_decimals/wc_get_rounding_precision calls on the checkout via StoreAPI hot path for better performance. \WC_Customer::save is a platform-level fix to avoid unnecessary customer object saves.

diff --git a/plugins/woocommerce/changelog/performance-checkout-thruput-storeapi-checkout b/plugins/woocommerce/changelog/performance-checkout-thruput-storeapi-checkout
new file mode 100644
index 00000000000..da046884e04
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-checkout-thruput-storeapi-checkout
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Optimized the order and customer save workflows during checkout using StoreAPI.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index 7412ae06e9f..e341b5c1338 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -2008,13 +2008,14 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		$shipping_total    = 0;
 		$cart_subtotal_tax = 0;
 		$cart_total_tax    = 0;
+		$price_decimals    = wc_get_price_decimals();

 		$cart_subtotal = $this->get_cart_subtotal_for_order();
 		$cart_total    = (float) $this->get_cart_total_for_order();

 		// Sum shipping costs.
 		foreach ( $this->get_shipping_methods() as $shipping ) {
-			$shipping_total += NumberUtil::round( $shipping->get_total(), wc_get_price_decimals() );
+			$shipping_total += NumberUtil::round( $shipping->get_total(), $price_decimals );
 		}

 		$this->set_shipping_total( $shipping_total );
@@ -2024,7 +2025,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 			$fee_total = (float) $item->get_total();

 			if ( 0 > $fee_total ) {
-				$max_discount = NumberUtil::round( $cart_total + $fees_total + $shipping_total, wc_get_price_decimals() ) * -1;
+				$max_discount = NumberUtil::round( $cart_total + $fees_total + $shipping_total, $price_decimals ) * -1;

 				if ( $fee_total < $max_discount && 0 > $max_discount ) {
 					$item->set_total( $max_discount );
@@ -2051,9 +2052,9 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 			}
 		}

-		$this->set_discount_total( NumberUtil::round( $cart_subtotal - $cart_total, wc_get_price_decimals() ) );
+		$this->set_discount_total( NumberUtil::round( $cart_subtotal - $cart_total, $price_decimals ) );
 		$this->set_discount_tax( wc_round_tax_total( $cart_subtotal_tax - $cart_total_tax ) );
-		$this->set_total( NumberUtil::round( $cart_total + $fees_total + (float) $this->get_shipping_total() + (float) $this->get_cart_tax() + (float) $this->get_shipping_tax(), wc_get_price_decimals() ) );
+		$this->set_total( NumberUtil::round( $cart_total + $fees_total + (float) $this->get_shipping_total() + (float) $this->get_cart_tax() + (float) $this->get_shipping_tax(), $price_decimals ) );

 		if ( $this->has_cogs() && $this->cogs_is_enabled() ) {
 			$this->calculate_cogs_total_value();
diff --git a/plugins/woocommerce/includes/class-wc-customer.php b/plugins/woocommerce/includes/class-wc-customer.php
index 8b0d2f7f13f..1fed289435c 100644
--- a/plugins/woocommerce/includes/class-wc-customer.php
+++ b/plugins/woocommerce/includes/class-wc-customer.php
@@ -1266,4 +1266,27 @@ class WC_Customer extends WC_Legacy_Customer {
 	public function set_is_paying_customer( $is_paying_customer ) {
 		$this->set_prop( 'is_paying_customer', (bool) $is_paying_customer );
 	}
+
+	/**
+	 * Overrides the save method to guard against saves with no data changed.
+	 *
+	 * @since 10.9.0
+	 * @return int
+	 */
+	public function save() {
+		$customer_id = $this->get_id();
+		if ( $customer_id ) {
+			$meta_data     = $this->meta_data ?? array();
+			$props_changed = ! empty( $this->password ) || ! empty( $this->changes );
+			$state_changed = $props_changed || ! empty( array_filter( $meta_data, static fn( $meta ) => ! $meta->id || ! empty( $meta->get_changes() ) ) );
+			if ( ! $state_changed ) {
+				// Backward compatibility: e.g. '( new WC_Customer( $customer_id ) )->save()' as means to trigger integrations.
+				do_action( 'woocommerce_update_customer', $customer_id, $this ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.HookCommentWrongStyle
+
+				return $this->get_id();
+			}
+		}
+
+		return parent::save();
+	}
 }
diff --git a/plugins/woocommerce/includes/class-wc-meta-data.php b/plugins/woocommerce/includes/class-wc-meta-data.php
index dd0e0c752f1..057adf6d0df 100644
--- a/plugins/woocommerce/includes/class-wc-meta-data.php
+++ b/plugins/woocommerce/includes/class-wc-meta-data.php
@@ -13,6 +13,10 @@ defined( 'ABSPATH' ) || exit;

 /**
  * Meta data class.
+ *
+ * @property int|null $id    Meta ID.
+ * @property string   $key   Meta key.
+ * @property mixed    $value Meta value.
  */
 class WC_Meta_Data implements JsonSerializable {

diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
index 742e1c83b08..80385dbf660 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
@@ -329,7 +329,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
 			'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available.
 		);

-		if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) {
+		if ( $meta_item->id && array_key_exists( $meta_item->id, $formatted_meta_data ) ) {
 			$formatted_meta_item = $formatted_meta_data[ $meta_item->id ];

 			$result['display_key']   = wc_clean( $formatted_meta_item->display_key );
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 39f82099818..0d8494c76ca 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -6,24 +6,6 @@ parameters:
 			count: 3
 			path: includes/abstracts/abstract-wc-data.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$id\.$#'
-			identifier: property.notFound
-			count: 2
-			path: includes/abstracts/abstract-wc-data.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 5
-			path: includes/abstracts/abstract-wc-data.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 6
-			path: includes/abstracts/abstract-wc-data.php
-
 		-
 			message: '#^Argument of an invalid type array\<WC_Meta_Data\>\|null supplied for foreach, only iterables are supported\.$#'
 			identifier: foreach.nonIterable
@@ -13050,12 +13032,6 @@ parameters:
 			count: 1
 			path: includes/class-wc-order-item-tax.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$id\.$#'
-			identifier: property.notFound
-			count: 1
-			path: includes/class-wc-order-item.php
-
 		-
 			message: '#^Access to an undefined property object\:\:\$key\.$#'
 			identifier: property.notFound
@@ -26154,24 +26130,6 @@ parameters:
 			count: 1
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$id\.$#'
-			identifier: property.notFound
-			count: 2
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 2
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 2
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php
-
 		-
 			message: '#^Call to an undefined method WC_Data\:\:calculate_totals\(\)\.$#'
 			identifier: method.notFound
@@ -43443,18 +43401,6 @@ parameters:
 			count: 1
 			path: src/Admin/API/Reports/Orders/Controller.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Admin/API/Reports/Orders/DataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Admin/API/Reports/Orders/DataStore.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore\:\:add_sql_query_params\(\) has no return type specified\.$#'
 			identifier: missingType.return
@@ -46473,24 +46419,6 @@ parameters:
 			count: 1
 			path: src/Admin/Features/Features.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$id\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Admin/Features/Fulfillments/DataStore/FulfillmentsDataStore.php
-
 		-
 			message: '#^Parameter \#2 \$meta \(WC_Meta_Data\) of method Automattic\\WooCommerce\\Admin\\Features\\Fulfillments\\DataStore\\FulfillmentsDataStore\:\:add_meta\(\) should be compatible with parameter \$meta \(stdClass\) of method WC_Data_Store_WP\:\:add_meta\(\)$#'
 			identifier: method.childParameterType
@@ -60543,18 +60471,6 @@ parameters:
 			count: 10
 			path: src/Internal/Admin/Orders/MetaBoxes/CustomMetaBox.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Internal/Admin/Orders/MetaBoxes/OrderAttribution.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Internal/Admin/Orders/MetaBoxes/OrderAttribution.php
-
 		-
 			message: '#^Cannot access property \$labels on WP_Taxonomy\|false\.$#'
 			identifier: property.nonObject
@@ -63753,24 +63669,6 @@ parameters:
 			count: 1
 			path: src/Internal/DataStores/Orders/OrdersTableDataStore.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$id\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Internal/DataStores/Orders/OrdersTableDataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 3
-			path: src/Internal/DataStores/Orders/OrdersTableDataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Internal/DataStores/Orders/OrdersTableDataStore.php
-
 		-
 			message: '#^Call to an undefined method WC_Abstract_Order\:\:get_address\(\)\.$#'
 			identifier: method.notFound
@@ -64410,24 +64308,6 @@ parameters:
 			count: 1
 			path: src/Internal/DataStores/Orders/OrdersTableQuery.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$id\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Internal/DataStores/Orders/OrdersTableRefundDataStore.php
-
 		-
 			message: '#^Access to an undefined property object\:\:\$meta_data\.$#'
 			identifier: property.notFound
@@ -65706,18 +65586,6 @@ parameters:
 			count: 1
 			path: src/Internal/Orders/OrderAttributionBlocksController.php

-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$key\.$#'
-			identifier: property.notFound
-			count: 2
-			path: src/Internal/Orders/OrderAttributionController.php
-
-		-
-			message: '#^Access to an undefined property WC_Meta_Data\:\:\$value\.$#'
-			identifier: property.notFound
-			count: 1
-			path: src/Internal/Orders/OrderAttributionController.php
-
 		-
 			message: '#^Callback expects 2 parameters, \$accepted_args is set to 10\.$#'
 			identifier: arguments.count
@@ -72606,12 +72474,6 @@ parameters:
 			count: 1
 			path: src/StoreApi/Routes/V1/Checkout.php

-		-
-			message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Checkout\:\:get_route_post_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
-			identifier: missingType.generics
-			count: 1
-			path: src/StoreApi/Routes/V1/Checkout.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\StoreApi\\Routes\\V1\\Checkout\:\:get_route_response\(\) has parameter \$request with generic class WP_REST_Request but does not specify its types\: T$#'
 			identifier: missingType.generics
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index 00ac3ad4899..f02982ffba9 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -355,11 +355,13 @@ class Checkout extends AbstractCartRoute {
 		 * Create (or update) Draft Order and process request data.
 		 */
 		$this->create_or_update_draft_order( $request );
+		// Order save-point: 1.

 		/**
 		 * Persist additional fields, order notes and payment method for order.
 		 */
 		$this->update_order_from_request( $request );
+		// Order save-point: 2.

 		if ( $request->get_param( '__experimental_calc_totals' ) ) {
 			/**
@@ -378,8 +380,6 @@ class Checkout extends AbstractCartRoute {
 			$this->cart_controller->validate_cart();
 		}

-		$this->order->save();
-
 		return $this->prepare_item_for_response(
 			(object) [
 				'order' => wc_get_order( $this->order ),
@@ -392,6 +392,29 @@ class Checkout extends AbstractCartRoute {
 	/**
 	 * Process an order.
 	 *
+	 * @throws RouteException On error.
+	 * @param \WP_REST_Request<array<string, mixed>> $request Request object.
+	 * @return \WP_REST_Response|\WP_Error
+	 */
+	protected function get_route_post_response( \WP_REST_Request $request ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
+		try {
+			return $this->process_order( $request );
+		} catch ( \Throwable $exception ) {
+			if ( $this->order ) {
+				// The optimistic order save bounced back, persist the order as it is to preserve the intermediate state.
+				try {
+					$this->order->save();
+				} catch ( \Throwable $save_exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+					// Ignore the save exception, the root cause will be bubbled up via re-throwing $exception.
+				}
+			}
+			throw $exception;
+		}
+	}
+
+	/**
+	 * Process an order based on optimistic save approach to minimize the number of order saves.
+	 *
 	 * 1. Obtain Draft Order
 	 * 2. Process Request
 	 * 3. Process Customer
@@ -399,12 +422,10 @@ class Checkout extends AbstractCartRoute {
 	 * 5. Process Payment
 	 *
 	 * @throws RouteException On error.
-	 *
-	 * @param \WP_REST_Request $request Request object.
-	 *
+	 * @param \WP_REST_Request<array<string, mixed>> $request Request object.
 	 * @return \WP_REST_Response|\WP_Error
 	 */
-	protected function get_route_post_response( \WP_REST_Request $request ) {
+	private function process_order( \WP_REST_Request $request ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
 		wc_log_order_step( '[Store API #1] Place Order flow initiated', null, false, true );

 		$validation_callback = $this->validate_callback( $request );
@@ -440,16 +461,19 @@ class Checkout extends AbstractCartRoute {
 		 * uses the up-to-date customer address.
 		 */
 		$this->update_customer_from_request( $request );
+		// Customer save-point: 1 (session-stored).
 		wc_log_order_step( '[Store API #3] Updated customer data from request' );

 		/**
 		 * Create (or update) Draft Order and process request data.
 		 */
 		$this->create_or_update_draft_order( $request );
+		// Order save-point: 1.
 		wc_log_order_step( '[Store API #4] Created/Updated draft order', array( 'order_object' => $this->order ) );
-		$this->update_order_from_request( $request );
+		$this->update_order_from_request( $request, false );
 		wc_log_order_step( '[Store API #5] Updated order with posted data', array( 'order_object' => $this->order ) );
 		$this->process_customer( $request );
+		// Customer save-point: 2 (db-stored; optional, guest -> customer transition or customer data has changed).
 		wc_log_order_step( '[Store API #6] Created and/or persisted customer data from order', array( 'order_object' => $this->order ) );

 		/**
@@ -522,13 +546,10 @@ class Checkout extends AbstractCartRoute {
 			'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_order_processed instead.'
 		);

-		// Set the order status to 'pending' as an initial step.
-		// This allows the order to proceed towards completion. The hook
-		// 'woocommerce_store_api_checkout_order_processed' (fired below) can be used
-		// to set a custom status *after* this point.
-		// If payment isn't needed, the custom status is kept. If payment is needed,
-		// the payment gateway's statuses take precedence.
+		// Set initial status to 'pending'; woocommerce_store_api_checkout_order_processed (fired below) can override it.
+		// Custom statuses are preserved when no payment is needed; payment gateway statuses take precedence otherwise.
 		$this->order->update_status( 'pending' );
+		// Order save-point: 2.

 		/**
 		 * Fires before an order is processed by the Checkout Block/Store API.
@@ -645,7 +666,6 @@ class Checkout extends AbstractCartRoute {
 		if ( ! $this->order ) {
 			$this->order = $this->order_controller->create_order_from_cart();
 			wc_log_order_step( '[Store API #4::create_or_update_draft_order] Created order from cart', array( 'order_object' => $this->order ) );
-
 		} else {
 			$this->order_controller->update_order_from_cart( $this->order, true );
 			wc_log_order_step( '[Store API #4::create_or_update_draft_order] Updated order from cart', array( 'order_object' => $this->order ) );
@@ -868,12 +888,10 @@ class Checkout extends AbstractCartRoute {

 			// Associate customer with the order.
 			$this->order->set_customer_id( $customer_id );
-			$this->order->save();

 			// Set the customer auth cookie.
 			wc_set_customer_auth_cookie( $customer_id );
 			wc_log_order_step( '[Store API #6::process_customer] Created new customer', array( 'customer_id' => $customer_id ) );
-
 		}

 		// Persist customer address data to account.
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php
index a7feb68c076..0052bb0620d 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/CartItemSchema.php
@@ -50,6 +50,7 @@ class CartItemSchema extends ItemSchema {
 		 * @param string $cart_item_key     Cart item key.
 		 */
 		$product_permalink = apply_filters( 'woocommerce_cart_item_permalink', $product->get_permalink(), $cart_item, $cart_item['key'] );
+		$price_decimals    = wc_get_price_decimals();

 		return [
 			'key'                  => $cart_item['key'],
@@ -72,10 +73,10 @@ class CartItemSchema extends ItemSchema {
 			'prices'               => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ),
 			'totals'               => (object) $this->prepare_currency_response(
 				[
-					'line_subtotal'     => $this->prepare_money_response( $cart_item['line_subtotal'], wc_get_price_decimals() ),
-					'line_subtotal_tax' => $this->prepare_money_response( $cart_item['line_subtotal_tax'], wc_get_price_decimals() ),
-					'line_total'        => $this->prepare_money_response( $cart_item['line_total'], wc_get_price_decimals() ),
-					'line_total_tax'    => $this->prepare_money_response( $cart_item['line_tax'], wc_get_price_decimals() ),
+					'line_subtotal'     => $this->prepare_money_response( $cart_item['line_subtotal'], $price_decimals ),
+					'line_subtotal_tax' => $this->prepare_money_response( $cart_item['line_subtotal_tax'], $price_decimals ),
+					'line_total'        => $this->prepare_money_response( $cart_item['line_total'], $price_decimals ),
+					'line_total_tax'    => $this->prepare_money_response( $cart_item['line_tax'], $price_decimals ),
 				]
 			),
 			'catalog_visibility'   => $product->get_catalog_visibility(),
diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
index 9941817d057..56becf4ae58 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/OrderItemSchema.php
@@ -124,11 +124,13 @@ class OrderItemSchema extends ItemSchema {
 	 * @return array
 	 */
 	public function get_totals( $order_item ) {
+		$price_decimals = wc_get_price_decimals();
+
 		return [
-			'line_subtotal'     => $this->prepare_money_response( $order_item->get_subtotal(), wc_get_price_decimals() ),
-			'line_subtotal_tax' => $this->prepare_money_response( $order_item->get_subtotal_tax(), wc_get_price_decimals() ),
-			'line_total'        => $this->prepare_money_response( $order_item->get_total(), wc_get_price_decimals() ),
-			'line_total_tax'    => $this->prepare_money_response( $order_item->get_total_tax(), wc_get_price_decimals() ),
+			'line_subtotal'     => $this->prepare_money_response( $order_item->get_subtotal(), $price_decimals ),
+			'line_subtotal_tax' => $this->prepare_money_response( $order_item->get_subtotal_tax(), $price_decimals ),
+			'line_total'        => $this->prepare_money_response( $order_item->get_total(), $price_decimals ),
+			'line_total_tax'    => $this->prepare_money_response( $order_item->get_total_tax(), $price_decimals ),
 		];
 	}

diff --git a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
index 46d4da98218..a59d00d3406 100644
--- a/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
+++ b/plugins/woocommerce/src/StoreApi/Schemas/V1/ProductSchema.php
@@ -921,6 +921,7 @@ class ProductSchema extends AbstractSchema {
 		$prices           = [];
 		$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
 		$price_function   = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
+		$price_decimals   = wc_get_price_decimals();

 		// If we have a variable product, get the price from the variations (this will use the min value).
 		if ( $product->is_type( ProductType::VARIABLE ) ) {
@@ -931,9 +932,9 @@ class ProductSchema extends AbstractSchema {
 			$sale_price    = $product->get_sale_price();
 		}

-		$prices['price']         = $this->prepare_money_response( $price_function( $product ), wc_get_price_decimals() );
-		$prices['regular_price'] = $this->prepare_money_response( $price_function( $product, [ 'price' => $regular_price ] ), wc_get_price_decimals() );
-		$prices['sale_price']    = $this->prepare_money_response( $price_function( $product, [ 'price' => $sale_price ] ), wc_get_price_decimals() );
+		$prices['price']         = $this->prepare_money_response( $price_function( $product ), $price_decimals );
+		$prices['regular_price'] = $this->prepare_money_response( $price_function( $product, [ 'price' => $regular_price ] ), $price_decimals );
+		$prices['sale_price']    = $this->prepare_money_response( $price_function( $product, [ 'price' => $sale_price ] ), $price_decimals );
 		$prices['price_range']   = $this->get_price_range( $product, $tax_display_mode );

 		return $this->prepare_currency_response( $prices );
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
index 1f357cb3f43..ca93d3c22ab 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/CheckoutTrait.php
@@ -139,8 +139,9 @@ trait CheckoutTrait {
 	 * Update the current order using the posted values from the request.
 	 *
 	 * @param \WP_REST_Request $request Full details about the request.
+	 * @param bool             $persist Whether to persist the changes right away (defaults to true).
 	 */
-	private function update_order_from_request( \WP_REST_Request $request ) {
+	private function update_order_from_request( \WP_REST_Request $request, bool $persist = true ) {
 		$this->order->set_customer_note( wc_sanitize_textarea( $request['customer_note'] ) ?? '' );
 		$payment_method = $this->get_request_payment_method( $request );
 		if ( null !== $payment_method ) {
@@ -201,7 +202,9 @@ trait CheckoutTrait {
 		 */
 		do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request );

-		$this->order->save();
+		if ( $persist ) {
+			$this->order->save();
+		}
 	}

 	/**
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
index 95c303abc16..4aeae5755c6 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/OrderController.php
@@ -127,8 +127,9 @@ class OrderController {
 	 * @param \WC_Order $order Order object.
 	 */
 	public function sync_customer_data_with_order( \WC_Order $order ) {
-		if ( $order->get_customer_id() ) {
-			$customer = new \WC_Customer( $order->get_customer_id() );
+		$customer_id = $order->get_customer_id();
+		if ( $customer_id ) {
+			$customer = new \WC_Customer( $customer_id );
 			$customer->set_props(
 				array(
 					'billing_first_name'  => $order->get_billing_first_name(),
@@ -154,9 +155,7 @@ class OrderController {
 					'shipping_phone'      => $order->get_shipping_phone(),
 				)
 			);
-
 			$this->additional_fields_controller->sync_customer_additional_fields_with_order( $order, $customer );
-
 			$customer->save();
 		}
 	}
diff --git a/plugins/woocommerce/src/StoreApi/Utilities/ProductItemTrait.php b/plugins/woocommerce/src/StoreApi/Utilities/ProductItemTrait.php
index 80aeeecf84b..da60dbba4c2 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/ProductItemTrait.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/ProductItemTrait.php
@@ -15,16 +15,17 @@ trait ProductItemTrait {
 	 * @return array
 	 */
 	protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
-		$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
-		$price_function   = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
-		$prices           = parent::prepare_product_price_response( $product, $tax_display_mode );
+		$tax_display_mode   = $this->get_tax_display_mode( $tax_display_mode );
+		$price_function     = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
+		$prices             = parent::prepare_product_price_response( $product, $tax_display_mode );
+		$rounding_precision = wc_get_rounding_precision();

 		// Add raw prices (prices with greater precision).
 		$prices['raw_prices'] = array(
-			'precision'     => wc_get_rounding_precision(),
-			'price'         => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ),
-			'regular_price' => $this->prepare_money_response( $price_function( $product, array( 'price' => $product->get_regular_price() ) ), wc_get_rounding_precision() ),
-			'sale_price'    => $this->prepare_money_response( $price_function( $product, array( 'price' => $product->get_sale_price() ) ), wc_get_rounding_precision() ),
+			'precision'     => $rounding_precision,
+			'price'         => $this->prepare_money_response( $price_function( $product ), $rounding_precision ),
+			'regular_price' => $this->prepare_money_response( $price_function( $product, array( 'price' => $product->get_regular_price() ) ), $rounding_precision ),
+			'sale_price'    => $this->prepare_money_response( $price_function( $product, array( 'price' => $product->get_sale_price() ) ), $rounding_precision ),
 		);

 		return $prices;