Commit 2ba45168d49 for woocommerce

commit 2ba45168d492d701fd865103dc4a69f84a53452b
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Fri Sep 25 14:42:38 2026 +0300

    Fix product rating aggregates for Review Order submissions (#69060)

    * Update product rating aggregates for Review Order submissions

    Reviews submitted or edited on the Review Order page were left out of
    the product's average rating and rating counts when approved straight
    away. Core recounts an approved comment in wp_insert_comment() before
    it stores comment_meta, and the edit path wrote the rating after
    wp_update_comment() had already recounted.

    - Recount the product right after an approved review is inserted.
    - Pass the rating to wp_update_comment() as comment_meta, which core
      stores before it recounts.

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

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/69060-wooplug-7804-customer-review-request-reviews-submitted-on-the-review b/plugins/woocommerce/changelog/69060-wooplug-7804-customer-review-request-reviews-submitted-on-the-review
new file mode 100644
index 00000000000..ccc0de3c9e2
--- /dev/null
+++ b/plugins/woocommerce/changelog/69060-wooplug-7804-customer-review-request-reviews-submitted-on-the-review
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Include reviews left on the Review Order page in the product's star rating and rating count.
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/SubmissionHandler.php b/plugins/woocommerce/src/Internal/OrderReviews/SubmissionHandler.php
index 6038fd4bb93..50973488a1a 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/SubmissionHandler.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/SubmissionHandler.php
@@ -8,6 +8,7 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Internal\OrderReviews;

 use Automattic\WooCommerce\Enums\OrderStatus;
+use WC_Comments;
 use WC_Order;

 /**
@@ -340,12 +341,18 @@ class SubmissionHandler {
 			// Tag a row the checks rejected (spam/trash) as an automatic rejection,
 			// so a later valid resubmission is treated as a fresh attempt rather
 			// than a moderator's final verdict by has_rejected_review().
-			if ( in_array( wp_get_comment_status( $comment_id ), array( 'spam', 'trash' ), true ) ) {
+			$comment_status = wp_get_comment_status( $comment_id );
+			if ( in_array( $comment_status, array( 'spam', 'trash' ), true ) ) {
 				update_comment_meta( (int) $comment_id, self::AUTO_REJECTED_META_KEY, 1 );
 			}

+			// Core recounts an approved comment before writing its meta, so recount once the rating is stored.
+			if ( 'approved' === $comment_status ) {
+				WC_Comments::clear_transients( $review_post_id );
+			}
+
 			$result['comment_id'] = (int) $comment_id;
-			$result['status']     = 'approved' === wp_get_comment_status( $comment_id ) ? 'ok' : 'pending_moderation';
+			$result['status']     = 'approved' === $comment_status ? 'ok' : 'pending_moderation';

 			$request_slot_comments[ $slot_key ] = $result;
 			$results[ $row_index ]              = $result;
@@ -384,6 +391,8 @@ class SubmissionHandler {
 				array(
 					'comment_ID'      => $existing_id,
 					'comment_content' => $content,
+					// Stored by core before it recounts the product's ratings.
+					'comment_meta'    => array( 'rating' => $rating ),
 				)
 			)
 		);
@@ -391,8 +400,6 @@ class SubmissionHandler {
 			return array( 'error' => 'update_failed' );
 		}

-		update_comment_meta( $existing_id, 'rating', $rating );
-
 		// Drive the status explicitly so a previously auto-rejected row is untrashed
 		// when it now approves or holds. Only when it actually changes, so an
 		// unchanged status doesn't fire a redundant transition (or approval notice).
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
index 4ab20e39f2c..f877ad32489 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
@@ -32,6 +32,7 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
 		update_option( 'comment_moderation', '0' );
 		update_option( 'comment_max_links', 2 );
 		update_option( 'moderation_keys', '' );
+		update_option( 'disallowed_keys', '' );
 		remove_all_filters( 'woocommerce_review_order_submitted' );
 		remove_all_filters( 'woocommerce_review_order_eligible_statuses' );
 		remove_all_filters( 'woocommerce_review_order_eligible_items' );
@@ -1437,4 +1438,204 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
 		);
 		$this->assertSame( 0, $total );
 	}
+
+	/**
+	 * Submit review rows for an order and return the per-row results.
+	 *
+	 * @param WC_Order $order The order being reviewed.
+	 * @param array    $rows  Rows with product_id, order_item_id, rating and text.
+	 * @return array Result rows, in submission order.
+	 */
+	private function submit_rows( WC_Order $order, array $rows ): array {
+		$_POST = array(
+			'order_id' => $order->get_id(),
+			'key'      => $order->get_order_key(),
+			'_wcnonce' => wp_create_nonce( SubmissionHandler::ACTION ),
+			'reviews'  => $rows,
+		);
+
+		$response = $this->dispatch();
+		$results  = array_values( (array) ( $response['data']['results'] ?? array() ) );
+		$this->assertCount( count( $rows ), $results, 'Each submitted row should produce a result.' );
+
+		return $results;
+	}
+
+	/**
+	 * Submit a single-row review for the given order item.
+	 *
+	 * @param WC_Order $order      The order being reviewed.
+	 * @param int      $product_id Product ID of the row.
+	 * @param int      $item_id    Order item ID of the row.
+	 * @param int      $rating     Star rating.
+	 * @param string   $text       Review text.
+	 * @return array Result row for the submitted review.
+	 */
+	private function submit_review( WC_Order $order, int $product_id, int $item_id, int $rating, string $text ): array {
+		$results = $this->submit_rows(
+			$order,
+			array(
+				array(
+					'product_id'    => $product_id,
+					'order_item_id' => $item_id,
+					'rating'        => $rating,
+					'text'          => $text,
+				),
+			)
+		);
+
+		return $results[0];
+	}
+
+	/**
+	 * @testdox An approved review is included in the product's average rating, rating counts and review count.
+	 */
+	public function test_approved_review_updates_product_rating_aggregates(): void {
+		$built      = $this->make_order( 1 );
+		$product_id = $built['product_ids'][0];
+
+		$row = $this->submit_review( $built['order'], $product_id, $built['item_ids'][0], 5, 'Great.' );
+		$this->assertSame( 'ok', $row['status'] );
+
+		$product = wc_get_product( $product_id );
+		$this->assertEquals( 5, (float) $product->get_average_rating() );
+		$this->assertSame( array( 5 => 1 ), $product->get_rating_counts() );
+		$this->assertSame( 1, $product->get_review_count() );
+	}
+
+	/**
+	 * @testdox Editing an approved review's rating updates the product aggregates to the new rating.
+	 * @testWith ["Great."]
+	 *           ["Changed my mind."]
+	 *
+	 * @param string $edited_text Text sent with the edit; the original text keeps the row's moderation state.
+	 */
+	public function test_editing_approved_review_updates_product_rating_aggregates( string $edited_text ): void {
+		$built      = $this->make_order( 1 );
+		$product_id = $built['product_ids'][0];
+		$item_id    = $built['item_ids'][0];
+
+		$first = $this->submit_review( $built['order'], $product_id, $item_id, 5, 'Great.' );
+		$edit  = $this->submit_review( $built['order'], $product_id, $item_id, 2, $edited_text );
+		$this->assertSame( 'ok', $edit['status'] );
+		$this->assertSame( $first['comment_id'], $edit['comment_id'] );
+
+		$product = wc_get_product( $product_id );
+		$this->assertEquals( 2, (float) $product->get_average_rating() );
+		$this->assertSame( array( 2 => 1 ), $product->get_rating_counts() );
+		$this->assertSame( 1, $product->get_review_count() );
+	}
+
+	/**
+	 * @testdox A review held for moderation is not counted until it is approved.
+	 */
+	public function test_held_review_is_counted_only_after_approval(): void {
+		update_option( 'comment_moderation', '1' );
+
+		$built      = $this->make_order( 1 );
+		$product_id = $built['product_ids'][0];
+
+		$row = $this->submit_review( $built['order'], $product_id, $built['item_ids'][0], 4, 'Pending.' );
+		$this->assertSame( 'pending_moderation', $row['status'] );
+		$this->assertSame( array(), wc_get_product( $product_id )->get_rating_counts() );
+
+		wp_set_comment_status( $row['comment_id'], 'approve' );
+
+		$product = wc_get_product( $product_id );
+		$this->assertEquals( 4, (float) $product->get_average_rating() );
+		$this->assertSame( array( 4 => 1 ), $product->get_rating_counts() );
+	}
+
+	/**
+	 * @testdox A held review is counted once an edit removes the text that held it.
+	 */
+	public function test_held_review_approved_by_edit_updates_product_rating_aggregates(): void {
+		update_option( 'moderation_keys', 'hold-this' );
+
+		$built      = $this->make_order( 1 );
+		$product_id = $built['product_ids'][0];
+		$item_id    = $built['item_ids'][0];
+
+		$held = $this->submit_review( $built['order'], $product_id, $item_id, 5, 'Please hold-this review.' );
+		$this->assertSame( 'pending_moderation', $held['status'] );
+
+		$edit = $this->submit_review( $built['order'], $product_id, $item_id, 3, 'Clean text now.' );
+		$this->assertSame( 'ok', $edit['status'] );
+		$this->assertSame( $held['comment_id'], $edit['comment_id'] );
+
+		$product = wc_get_product( $product_id );
+		$this->assertEquals( 3, (float) $product->get_average_rating() );
+		$this->assertSame( array( 3 => 1 ), $product->get_rating_counts() );
+		$this->assertSame( 1, $product->get_review_count() );
+	}
+
+	/**
+	 * @testdox Reviews for two variations in one submission are both included in the parent product's aggregates.
+	 */
+	public function test_variation_reviews_update_parent_aggregates(): void {
+		$variable      = WC_Helper_Product::create_variation_product();
+		$variation_ids = $variable->get_children();
+		$variation_a   = wc_get_product( $variation_ids[0] );
+		$variation_b   = wc_get_product( $variation_ids[1] );
+
+		$order = $this->make_empty_order();
+		$order->add_product( $variation_a, 1 );
+		$order->add_product( $variation_b, 1 );
+		$order->save();
+		$items = array_values( $order->get_items() );
+
+		$results = $this->submit_rows(
+			$order,
+			array(
+				array(
+					'product_id'    => $variation_a->get_id(),
+					'order_item_id' => $items[0]->get_id(),
+					'rating'        => 5,
+					'text'          => 'Loved variation A.',
+				),
+				array(
+					'product_id'    => $variation_b->get_id(),
+					'order_item_id' => $items[1]->get_id(),
+					'rating'        => 3,
+					'text'          => 'Variation B was just OK.',
+				),
+			)
+		);
+		$this->assertSame( array( 'ok', 'ok' ), array_column( $results, 'status' ) );
+
+		$parent = wc_get_product( $variable->get_id() );
+		$this->assertEquals( 4, (float) $parent->get_average_rating() );
+		$this->assertSame(
+			array(
+				3 => 1,
+				5 => 1,
+			),
+			$parent->get_rating_counts()
+		);
+		$this->assertSame( 2, $parent->get_review_count() );
+	}
+
+	/**
+	 * @testdox A review auto-rejected for a disallowed word is counted once a clean resubmission approves it.
+	 */
+	public function test_resubmission_after_auto_rejection_updates_product_rating_aggregates(): void {
+		update_option( 'disallowed_keys', 'casino-bonus' );
+
+		$built      = $this->make_order( 1 );
+		$product_id = $built['product_ids'][0];
+		$item_id    = $built['item_ids'][0];
+
+		$rejected = $this->submit_review( $built['order'], $product_id, $item_id, 5, 'Visit casino-bonus now.' );
+		$this->assertContains( wp_get_comment_status( $rejected['comment_id'] ), array( 'spam', 'trash' ) );
+		$this->assertSame( array(), wc_get_product( $product_id )->get_rating_counts() );
+
+		$resubmitted = $this->submit_review( $built['order'], $product_id, $item_id, 4, 'Clean text this time.' );
+		$this->assertSame( 'ok', $resubmitted['status'] );
+		$this->assertSame( $rejected['comment_id'], $resubmitted['comment_id'] );
+
+		$product = wc_get_product( $product_id );
+		$this->assertEquals( 4, (float) $product->get_average_rating() );
+		$this->assertSame( array( 4 => 1 ), $product->get_rating_counts() );
+		$this->assertSame( 1, $product->get_review_count() );
+	}
 }