Commit e3ab3f7f04c for woocommerce
commit e3ab3f7f04cb9c8c2a46f900efc809323eac0e1d
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Thu Oct 8 19:06:42 2026 +0300
Add developer docs for the Customer Review Request feature (#69550)
diff --git a/docs/features/email/customer-review-request.md b/docs/features/email/customer-review-request.md
new file mode 100644
index 00000000000..22f1e029f04
--- /dev/null
+++ b/docs/features/email/customer-review-request.md
@@ -0,0 +1,156 @@
+---
+post_title: Customer review request email
+sidebar_label: Customer review request
+---
+
+# Customer Review Request
+
+## Overview
+
+The Customer Review Request feature emails customers a few days after their order is marked completed, asking them to review the products they bought. The email links to a dedicated tokenized landing page where the customer rates each eligible product item, optionally writes a review, and submits everything in a single form.
+
+The feature lives in the `Automattic\WooCommerce\Internal\OrderReviews` namespace and registers its WordPress hooks only when the feature flag is on.
+
+## Feature flag
+
+The feature ships **off by default** behind the `customer_review_request` feature flag (beta). Merchants enable it under **WooCommerce → Settings → Advanced → Features**. While the flag is off, none of the feature's hooks run, the review-request email is not registered with `WC_Emails`, and no host page is created. Enabling it registers the hooks and seeds the host page (see "Host page" below).
+
+## How it works
+
+When the feature is enabled, these pieces register their hooks:
+
+- **Scheduler** — on order completion, schedules the delayed email via Action Scheduler, and cancels it if the order later leaves the eligible set (cancelled, refunded, processing, on-hold, pending, failed, trashed, deleted). It also skips scheduling when nothing on the order is reviewable (see "Skip when nothing is reviewable" below).
+- **Endpoint** — routes `/review-order/{id}/?key={order_key}` to the WC-managed Review Order page and renders it through the `[woocommerce_review_order]` shortcode. Access requires a matching order key and an eligible status; for an order that has a customer id, a logged-in visitor must be that customer (guests, and anyone holding the key for a customer-less guest order, still pass).
+- **SubmissionHandler** — the AJAX endpoint that stores submitted reviews, one per `(product, variation)` slot, honoring the `comment_moderation` option and updating a review in place when the customer resubmits the same row.
+- **ItemEligibility** — decides which line items are reviewable, excludes fully-refunded items, and keeps each variation's row independent.
+
+`StarRating` and `Meta` are stateless view helpers the templates call directly.
+
+## Host page
+
+When the feature is enabled, WooCommerce creates and maintains a dedicated Review Order page (slug `review-order`, embedding the `[woocommerce_review_order]` shortcode). It is seeded by any "create default WooCommerce pages" run — fresh install, activation, database upgrade, and **Status → Tools → Create default WooCommerce pages** — and is self-healing: if the page is unpublished or the stored page option is re-pointed, it is restored on the next load. The page is hidden from nav menus and `wp_list_pages()` / `core/page-list` output, and is labeled "— Review Order Page" in the admin Pages list (matching how Cart / Checkout / Shop / My account are labeled). Its theme-rendered page title is suppressed so it does not duplicate the body `<h1>`.
+
+## Skip when nothing is reviewable
+
+If an order would land on the empty "nothing to review" page anyway, no email is scheduled or sent. At schedule time the Scheduler skips the order and logs the reason `no reviewable items`; at send time the email re-checks and is silently dropped if every item has since been reviewed or had reviews disabled (for example, the admin turns off reviews during the delay window).
+
+A customer can still reach the empty-state page directly — by finishing or losing their reviewable items after the email went out, or by opening a bookmarked or admin-shared link — so `customer-review-order-empty.php` keeps rendering that fallback.
+
+## Per-variation reviews
+
+The Review Order page renders one row per reviewable slot — a distinct `(product, variation)` pair — rather than one per raw line item. `customer-review-order.php` passes the items through `ItemEligibility::unique_slot_items()` first, so two line items of the same variation collapse into a single row. Two *different* variations of one variable product (e.g. a T-shirt purchased in Small + Medium) still get two distinct rows, each with its own image and attribute summary, and each can carry an independent rating + text review.
+
+Storage is on the parent product post — WC reviews don't live on variations — but the line item context is captured via two pieces of comment meta written by `SubmissionHandler` alongside `_review_order_id`:
+
+- `_review_variation_id` — the line item's `variation_id` (`0` for simple products, kept for symmetry).
+- `_review_variation_summary` — the attribute summary at the moment the review was written (e.g. `"Size: Small, Colour: Red"`). Captured as a snapshot so historical reviews stay readable even if the variation is later retired or its attribute terms are renamed.
+
+`ItemEligibility::find_existing_review()` keys by `(order_id, product_id, variation_id, email)`, so each variation row pre-fills against its own review on revisit. The Review Order row template renders the variation's attribute summary via `ItemEligibility::format_variation_summary()`, which restricts the attributes to the live variation's whitelisted slugs before passing them to `wc_get_formatted_variation()` — third-party item meta (engraving, gift-wrap, custom add-ons) is intentionally excluded so private personalisation data can't leak into the public review snapshot.
+
+On the parent product's single-product Reviews tab, the variation summary is prepended above each comment body through the existing `woocommerce_review_before_comment_text` action; reviews without the summary meta render unchanged. The theming class is `.woocommerce-review__variation-summary`.
+
+## Tokenized URL helper
+
+```php
+$url = wc_get_review_order_url( $order );
+```
+
+Returns the tokenized review-order URL for a given `WC_Order`. Use this rather than constructing the URL manually so the helper's filter and pretty/plain-permalink fallback both apply.
+
+## Filter and action reference
+
+### Filters
+
+- `woocommerce_should_send_review_request` (`bool`, `WC_Order`)
+ Return `false` to skip scheduling the email for a specific order.
+
+- `woocommerce_review_request_delay_seconds` (`int`)
+ Override the delay (in seconds) before the email fires. Defaults to the value configured on the email settings screen.
+
+- `woocommerce_review_order_url` (`string`, `WC_Order`)
+ Replace the URL emitted by `wc_get_review_order_url()`.
+
+- `woocommerce_review_order_eligible_statuses` (`string[]`, `WC_Order|null`)
+ The order statuses treated as eligible. Defaults to `[ 'completed' ]`. The same list is consulted at four points: the route-level page gate, the AJAX submission handler, the send-time re-check, and the scheduler's cancellation (a queued email is unscheduled only when the order leaves this set). Initial scheduling stays tied to the `completed` transition, so widening this filter does not queue a request when an order enters the added status; it only keeps a queued request alive through transitions inside the expanded set and lets the page and send pass. The second argument is the order, or `null` when the scheduler evaluates a status change without a resolved order.
+
+- `woocommerce_review_order_eligible_items` (`WC_Order_Item[]`, `WC_Order`)
+ The order items considered reviewable. The default callback excludes fully-refunded items. Beyond choosing which rows the page renders, it feeds the actionable-items check at both schedule time and send time, so emptying it for an order suppresses scheduling and sending too.
+
+- `woocommerce_review_order_rating_labels` (`array<int,string>`)
+ Customize the 1-5 star labels surfaced beside the control. Defaults to `Very poor / Not that bad / Average / Good / Perfect`.
+
+### Actions
+
+#### Send pipeline
+
+- `woocommerce_send_review_request` (`int $order_id`)
+ Action Scheduler hook. `Scheduler` enqueues this for each eligible order when the order moves to `completed`, with the delay configured on the email settings screen. When the delay elapses, Action Scheduler fires the hook and WC's transactional-email pipeline re-dispatches it as `woocommerce_send_review_request_notification` (the hook is registered with the pipeline in `WC_Emails::init_transactional_emails()`). Useful for plugins that need to observe the scheduled send without owning the email itself. It is a plain `do_action`, so a callback's return value cannot cancel the send — to prevent a send, use the `woocommerce_should_send_review_request` filter (evaluated at schedule time) instead.
+
+- `woocommerce_send_review_request_notification` (`int $order_id`)
+ Transactional email pipeline action, fired by `WC_Emails` after `woocommerce_send_review_request`. `WC_Email_Customer_Review_Request::trigger()` listens here and builds + sends the email. Useful when you need to add a second listener (e.g. analytics) alongside the built-in mailer.
+
+#### Form
+
+- `woocommerce_review_order_form_fields` (`WC_Order_Item_Product`, `WC_Product`, `WC_Order`, `int $row_index`)
+ Fires after each row's image / rating / review columns, as a sibling of `.woocommerce-review-order__item-row` and immediately before `</li>`. Echo extra fields directly; they render below the row's columns so injected UI doesn't disturb the three-column layout.
+
+- `woocommerce_review_order_submitted` (`WC_Order $order`, `array $results`)
+ Fires after the form has been processed, even when some rows ended in `error`. `$results` is the per-row outcome map keyed by row index, with `product_id`, `variation_id`, `status` (`ok | pending_moderation | error`), and (on success) `comment_id`.
+
+## Post-submit thank-you flow
+
+After a successful AJAX submission the thank-you view is shown in place of the form, re-using the empty-state copy. If any row failed, the form stays visible so the per-row error notes remain readable.
+
+Refreshing the page or re-clicking the email link re-renders the form for any remaining items — server-side routing is unchanged, so there is no URL hop and no server-side flag.
+
+## Theme overrides
+
+The page renders through `wc_get_template()`, so themes can override any of:
+
+- `templates/order/customer-review-order.php` — the page wrapper. Branches between the form view and the empty-state thank-you view, and pre-computes the per-row decisions consumed by the row template.
+- `templates/order/customer-review-order-row.php` — one form row per item. Receives `existing_rating` and `existing_text` so the row pre-fills when the customer already submitted a review for this order.
+- `templates/order/customer-review-order-empty.php` — thank-you view rendered when no actionable rows remain (every item already reviewed for this order, or every item skipped because reviews are disabled).
+- `templates/order/star-rating.php` — the accessible star-rating control partial.
+
+To override, copy a template into your theme while preserving the relative path:
+
+- `templates/order/customer-review-order.php` → `yourtheme/woocommerce/order/customer-review-order.php`
+- `templates/order/customer-review-order-row.php` → `yourtheme/woocommerce/order/customer-review-order-row.php`
+- `templates/order/customer-review-order-empty.php` → `yourtheme/woocommerce/order/customer-review-order-empty.php`
+- `templates/order/star-rating.php` → `yourtheme/woocommerce/order/star-rating.php`
+
+### Theme-aware classes used by the page
+
+The templates lean on built-in WC/WP classes so the active theme drives the styling instead of opinionated SCSS:
+
+- The disabled-products notice composes with `.woocommerce-info` — themes that already restyle classic WC notices automatically restyle this one too.
+- The customer/order meta line uses `.woocommerce-breadcrumb` so it adopts the theme's small-grey-secondary-text treatment.
+- The Submit button picks up `wc_wp_theme_get_element_class_name( 'button' )` (i.e. `wp-element-button` on block themes), so block themes paint it with their button styling.
+
+If you do override `customer-review-order.php`, preserve those class names so theme integrators don't have to re-style the page from scratch.
+
+## Email template overrides
+
+The HTML and plain-text bodies of the email itself follow standard WC conventions:
+
+- `templates/emails/customer-review-request.php`
+- `templates/emails/plain/customer-review-request.php`
+- `templates/emails/block/customer-review-request.php` (block-based email editor)
+
+## Tests
+
+PHPUnit tests live under `plugins/woocommerce/tests/php/src/Internal/OrderReviews/` (plus the email-class test under `plugins/woocommerce/tests/php/includes/emails/`); the Playwright specs are in `plugins/woocommerce/tests/e2e/tests/order/review-order-page.spec.ts`.
+
+```bash
+pnpm --filter=@woocommerce/plugin-woocommerce test:php:env -- --filter "OrderReviews|Customer_Review_Request"
+pnpm --filter=@woocommerce/plugin-woocommerce test:e2e:default --grep "Customer Review Request — Review Order page"
+```
+
+## Accessibility notes
+
+- The star-rating control is a group of five native `<input type="radio">` elements inside a wrapper that carries the ARIA `role="radiogroup"` and `aria-labelledby` pointing at the rating label. The visual stars are SVG; the inputs themselves are visually hidden but remain in the accessibility tree.
+- Keyboard navigation is supported via Arrow keys and Home/End.
+- A live caption (`aria-live="polite"`) announces the selected rating's label.
+- The "Required" indicator on the rating label is exposed to screen readers via a `.screen-reader-text` span alongside the visual asterisk.
+- Inline mandatory-rating errors render with `role="alert"` so assistive tech announces them on submission.
+- The host-page heading suppression keeps the chrome `<h1>` from duplicating the body `<h1>`, avoiding the "page announced twice" issue with screen readers.