Commit 45076b80178 for woocommerce
commit 45076b80178cd2337b9470d7c0816d62907704ce
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date: Wed May 13 02:03:04 2026 +0300
Exclude fully-refunded items from Review Order page (#64531)
* Add accessible star-rating control for Review Order page
Native `<input type="radio">` ✕ 5 wrapped in a `role="radiogroup"` container,
visually replaced by SVG stars. Progressive enhancement adds:
- Keyboard nav: ArrowLeft/Up, ArrowRight/Down, Home, End. Selecting also
moves focus and dispatches `change`, so any future submission listener
reacts naturally.
- Dynamic caption underneath the stars, populated from the focused/checked
radio's `data-label`. Defaults match the existing WC product-review
labels (Very poor / Not that bad / Average / Good / Perfect) and are
filterable via `woocommerce_review_order_rating_labels`.
- High-contrast focus ring on the wrapping label whenever its input is
`:focus-visible`.
- Hover preview lights stars left of the cursor; selection persists when
the cursor leaves.
Without JavaScript the underlying radio group still works as a plain
form field, so the page stays functional.
Files:
- `src/Internal/OrderReviews/StarRating.php` — server-side renderer.
Builds the markup and exposes `get_labels()` (filter-aware, with safe
fallbacks for buggy filters that drop slot keys).
- `templates/order/star-rating.php` — theme-overridable partial.
- `client/legacy/js/frontend/order-review.js` — vanilla JS, no jQuery.
- `client/legacy/css/order-review.scss` — visual states.
- `Internal/OrderReviews/Endpoint::enqueue_assets()` — script + style
enqueued only when the review-order endpoint is actually rendering.
- `templates/order/customer-review-order.php` — temporary preview render
per item so reviewers can interact with the control. M4 (WOOPLUG-6595)
swaps this preview for the real per-item form row.
Tests:
- `Internal\OrderReviews\StarRatingTest` — markup, defaults, filter
override, filter fallback for missing keys.
Closes #64308 (WOOPLUG-6594).
* Add changefile(s) from automation for the following project(s): woocommerce
* Remove duplicate changelog file (workflow generates the canonical one)
* Address Copilot feedback on star-rating control PR
- Route enqueue URLs through woocommerce_get_asset_url filter so
CDN/asset-rewrite setups continue to work.
- Add screen-reader-only 'Required' text alongside the visual asterisk
on the rating label.
- Correct the JS file header comment: only Arrow keys + Home/End are
handled, not Space/Enter.
* Fix StarRating docblock to use flat @param syntax
The PHPDoc shape syntax (array{...}) trips the WC PHPCS sniff, which
expects a single @param line per parameter. Reverted to plain
@param array with the key descriptions in the body.
* Drop the planning comment + minor tidy on star-rating styles
* Load the RTL stylesheet variant on RTL sites
wp_style_add_data( 'wc-order-review', 'rtl', 'replace' ) tells the
classic-assets pipeline to swap order-review.css for order-review-rtl.css
on right-to-left locales.
* Trim defensive fallbacks and dedupe asset enqueue helpers
- StarRating::get_labels(): replace the manual 1-5 fallback loop and
reorder block with array_replace + array_intersect_key.
- StarRating::render(): drop the unused $selected pre-cast; read it
inline only when it's used.
- Endpoint::enqueue_assets(): factor a local closure for the
woocommerce_get_asset_url filter so we don't repeat the same call
for the style and the script.
- order/star-rating.php template: drop the data-caption-id attribute
that duplicated aria-describedby.
- order-review.js: read the caption id from aria-describedby and drop
the empty-input early-out (init() already filters by class).
No behaviour change. All six StarRating tests still pass.
* Address review feedback on the star-rating control
- Add @since 10.8.0 annotations on StarRating::render() and
StarRating::get_labels() docblocks (per ayushpahwa).
- Bound the `selected` argument: values outside 0-5 fall back to
no-selection rather than passing an unrenderable value to the
template (per ayushpahwa).
- Cover the bound with a regression test.
* Address remaining review feedback on the star-rating control
- CSS: drop every `:has()` selector. The template now renders the
inputs in reverse DOM order (5..1) and uses `flex-direction:
row-reverse`; selected/hover/focus states are driven by the `~`
sibling combinator and `:focus-visible + label`. Caption gets
`order: -1` so it stays on the visual right.
- JS: keyboard navigation flips so ArrowRight/Down still moves
visually right (DOM-previous) and Home/End map to the visual
leftmost/rightmost stars.
- Template: pre-compute `$initial_caption` so the caption element no
longer mixes selection logic, escaping, and output in a single
expression.
- Endpoint: enqueue the JS with `strategy => defer` (in addition to
`in_footer => true`) since it only attaches DOM-ready listeners.
* Add accessible star-rating control for Review Order page
Native `<input type="radio">` ✕ 5 wrapped in a `role="radiogroup"` container,
visually replaced by SVG stars. Progressive enhancement adds:
- Keyboard nav: ArrowLeft/Up, ArrowRight/Down, Home, End. Selecting also
moves focus and dispatches `change`, so any future submission listener
reacts naturally.
- Dynamic caption underneath the stars, populated from the focused/checked
radio's `data-label`. Defaults match the existing WC product-review
labels (Very poor / Not that bad / Average / Good / Perfect) and are
filterable via `woocommerce_review_order_rating_labels`.
- High-contrast focus ring on the wrapping label whenever its input is
`:focus-visible`.
- Hover preview lights stars left of the cursor; selection persists when
the cursor leaves.
Without JavaScript the underlying radio group still works as a plain
form field, so the page stays functional.
Files:
- `src/Internal/OrderReviews/StarRating.php` — server-side renderer.
Builds the markup and exposes `get_labels()` (filter-aware, with safe
fallbacks for buggy filters that drop slot keys).
- `templates/order/star-rating.php` — theme-overridable partial.
- `client/legacy/js/frontend/order-review.js` — vanilla JS, no jQuery.
- `client/legacy/css/order-review.scss` — visual states.
- `Internal/OrderReviews/Endpoint::enqueue_assets()` — script + style
enqueued only when the review-order endpoint is actually rendering.
- `templates/order/customer-review-order.php` — temporary preview render
per item so reviewers can interact with the control. M4 (WOOPLUG-6595)
swaps this preview for the real per-item form row.
Tests:
- `Internal\OrderReviews\StarRatingTest` — markup, defaults, filter
override, filter fallback for missing keys.
Closes #64308 (WOOPLUG-6594).
* Add per-item review form row and submit gate
Replaces the temporary preview render from #64525 with the real per-item
form that the Customer Review Request page submits to. Submission handler
itself lands in M4 (WOOPLUG-6596).
`templates/order/customer-review-order-row.php` (new, theme-overridable):
- Linked product title that opens the product page in a new tab.
- 120×120 thumbnail.
- Hidden `product_id` and `order_item_id` so the row is identifiable
on submit.
- The accessible star-rating control from #64525, indexed by `row_index`
for stable POST data.
- Textarea labelled "Your review" with the i18n placeholder
"Share your experience with this product...".
- `do_action( 'woocommerce_review_order_form_fields', $item, $product,
$order, $row_index )` after the textarea so extensions can inject
fields without overriding the whole template.
`templates/order/customer-review-order.php`:
- Wraps the item list in `<form class="woocommerce-review-order__form"
method="post" action="">` with a hidden `order_id`, hidden `key`
(echoed back from the URL the Endpoint already validated), and the
`woocommerce_submit_order_reviews` nonce.
- "Submit reviews" button rendered disabled by default. The JS module
enables it as soon as any row has a rating selected.
`client/legacy/js/frontend/order-review.js` gains `initSubmitGate(form)`,
which listens for `change` events on `.woocommerce-star-rating__input`
inside the form and toggles the button's `disabled` state.
- StarRating's docblock simplified to plain `@param array` so the WC
PHPCS sniffs don't misparse the structured array shorthand.
Closes #64309 (WOOPLUG-6595).
* Add changefile(s) from automation for the following project(s): woocommerce
* Drop M-numbered milestone references from per-item row templates
* Strip non-layout styling from per-item row; let theme drive typography
* Stack the per-item row vertically on viewports under 600px
Folded in from the (now-closed) design-tokens-responsive PR; the only
contribution that survived the simplification was the 12-line media
query, which fits naturally with the per-item layout introduced here.
* Add changefile(s) from automation for the following project(s): woocommerce
* Wire customer-review-order.php to the per-item row partial
The 6595 changes had been dropped during cascading rebases: the form
wrapper, hidden inputs, foreach loop calling
wc_get_template('order/customer-review-order-row.php', ...), and the
submit button were all missing. Restored so each item actually renders
the row partial and the submit gate's selectors exist in the DOM.
* Strengthen the new-tab product link
- rel='noopener noreferrer' (was just 'noopener') to also prevent
referrer leakage in some browsers/configurations.
- Add a .screen-reader-text 'opens in a new tab' suffix so assistive
tech users are warned the link spawns a new window.
* Address per-item-row review feedback
- Collapse the .woocommerce-review-order__item top border to a single
declaration; the previous form layered border-top-color over a
currentColor border, which was redundant.
- Log a console.warn from initSubmitGate when the submit button is
missing so theme overrides that drop it surface the cause.
* Wrap the long console.warn message to satisfy eslint max-len
* Add AJAX submission handler for the Review Order form
`Internal\OrderReviews\SubmissionHandler` registers
`wp_ajax{,_nopriv}_woocommerce_submit_order_reviews`. For each rated row
it inserts a `comment_type='review'` comment against the product, with
`rating` (1-5) and `verified=1` commentmeta. Author fields are pulled
from the order; logged-in customers must own the order, while guests
authenticate via the order key.
Per-row outcome is `ok`, `pending_moderation`, or `error`. One row's
failure does not block the rest. The response shape:
```json
{
"success": true,
"data": {
"results": {
"<row_index>": {
"product_id": 123,
"status": "ok",
"comment_id": 456
}
}
}
}
```
`get_option('comment_moderation')` is honoured: when on, comments are
inserted with `comment_approved=0` and the row reports
`pending_moderation`.
`woocommerce_review_order_submitted` action fires once per submission
with the order and the per-row results array.
`_wc_review_request_completed_at` order meta is set when every line
item on the order has at least one comment by the customer's billing
email — no matter whether those reviews were posted in this submission
or earlier.
JS:
- `client/legacy/js/frontend/order-review.js` intercepts the form's
`submit`, posts via `fetch` to admin-ajax, renders per-row status
messages from the JSON response.
- The form's `action` falls back to admin-ajax.php for the no-JS case
(handler still works server-side).
Wrapper:
- `OrderReviews::init()` now lists `SubmissionHandler` as a third arg,
so the container resolves and auto-init's it. No `class-woocommerce.php`
changes needed.
Tests:
- `Internal\OrderReviews\SubmissionHandlerTest` covers bad nonce, bad
key, valid insert + meta, empty-rating skip, `comment_moderation`,
per-row isolation when one row references a product not on the
order, and the completed-at meta on full vs partial submission.
Closes #64310 (WOOPLUG-6596).
* Add changefile(s) from automation for the following project(s): woocommerce
* Address Copilot feedback on AJAX submission handler PR
- Gate submissions on the same woocommerce_review_order_eligible_statuses
filter the page-load endpoint uses, so a tampered POST with a valid key
cannot bypass status restrictions.
- Stop double-unslashing the row text. handle() already unslashes the
whole reviews array; process_rows() trusted that.
- Make maybe_mark_order_complete() idempotent: skip when the meta is
already set, and replace the per-product N+1 lookup with a single
grouped get_comments() call.
- Localize the JS status messages via wp_localize_script() instead of
hardcoding English strings client-side.
- Test housekeeping: remove the AJAX/JSON filters in tearDown(), and add
a test for the woocommerce_review_order_submitted action plus an
ineligible-status rejection test.
- Sanitize key and nonce inputs explicitly so the WordPress sniffs pass
on the changed lines.
* Drop empty meta_query arg that triggers WC slow-query lint
The empty meta_query => array() option was redundant and the WC sniff
flags the key's presence as a possible slow query.
* Address Copilot feedback (round 2) on AJAX submission handler PR
- Out-of-range ratings (e.g. 6) no longer disappear: only rating==0 is
treated as a deliberate skip; values <1 or >5 now report an
invalid_rating error per row.
- maybe_mark_order_complete() fetches comment objects directly so the
comment_post_ID lookup doesn't need a follow-up get_comment() per
row (was a regression of the original N+1).
- Submit gate is now reused after AJAX completion: expose syncSubmit
on the form, and initAjaxSubmit calls it instead of unconditionally
enabling the button. The button stays disabled when no rows have a
rating selected (e.g. after a failed submit + cleared rating).
* Add per-row status note style; source colors from WC --wc-green / --wc-red
Avoids introducing new --wc-review-order-* tokens. The active theme
controls everything else; we only need ok/error tinting on the inline
status message and we use the WC variables that already exist at :root.
* Point the Review Order form at admin-ajax for the new submission handler
Adds action + data-ajax-url + the action hidden input so the JS submit
gate's fetch() target and the no-JS POST both reach the handler.
* Accept variation_id; exclude spam/trash from completion check
- The Review Order row template posts the variation id for variable
products, but $item->get_product_id() returns the parent. Accept
either match and store the review against the parent so it shows on
the product page regardless of which variation was bought.
- Switch the completion lookup from status=>all to status=>[approve,hold]
so spam/trash reviews can't trip _wc_review_request_completed_at.
- Fix the maybe_mark_order_complete() docblock so its 'verified review'
wording matches the actual behaviour (any non-spam/non-trash review
by the customer counts).
* Filter the network-failure error path to rows with a rating
The catch handler renders an error on every item row, but the earlier
'success: false' branch only flags rows that were actually rated. A
network failure mid-submit now incorrectly tells the customer the rows
they intentionally skipped also failed; mirror the existing 'has a
checked rating' guard so only rated rows surface the error.
* Add already-reviewed locked row state
When the customer has already reviewed a product on the order (matched
by billing email), the row renders a static "Reviewed" summary instead
of the form. Reviews disabled on a product cause the row to be skipped
entirely.
Files:
- `src/Internal/OrderReviews/ItemEligibility.php` — `describe()` returns
one of `STATUS_FORM | STATUS_REVIEWED | STATUS_SKIP` plus the matched
comment when applicable. Wraps the decision in
`woocommerce_review_order_item_already_reviewed` so extensions can
flip either direction.
- `templates/order/customer-review-order-row-reviewed.php` — locked row
partial: linked product title, thumbnail, "Reviewed" badge with
posted-on date, locked star rating + label, truncated review text,
optional "View on product page" link.
- `templates/order/customer-review-order.php` — dispatch on the
`ItemEligibility` decision; calls the existing form-row partial for
pending items and the new reviewed-row partial for matches.
- `client/legacy/css/order-review.scss` — reviewed-row styles
(badge, locked rating layout, blockquote-style review text).
- `tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php` —
defaults, comments-closed, existing-match, different-author, filter
override, filter-args context.
Drive-by: tightened lint warnings in `SubmissionHandler` so branch lint
passes against trunk (alignment, yoda conditions, end-comments).
Closes #64312 (WOOPLUG-6598).
* Add changefile(s) from automation for the following project(s): woocommerce
* Address Copilot feedback on already-reviewed locked row PR
- Eliminate the N+1 query: ItemEligibility::prime() pre-fills a
per-request cache with a single get_comments() call covering every
product on the order, and find_existing_review() now reads that
cache. The template calls prime() once before iterating items.
- Tolerate the filter-only reviewed state: when the
woocommerce_review_order_item_already_reviewed filter forces true
without a backing comment, the locked row template renders the
reviewed state without trying to access comment fields.
- Add tests for both behaviors (prime-caches-results and
filter_forced_reviewed_with_no_comment).
* Remove only this test's comments_pre_query filter, not all of them
remove_all_filters( 'comments_pre_query' ) wiped any other callbacks
on a core hook, which could leak state to neighbouring tests. Hold a
reference to the counter and remove only that one.
* Add quote indent for the locked Reviewed row text
* Close server-side gaps in the Review Order submission handler
Previously the handler validated the row's order_item_id and that the
submitted product_id matched the line item's product_id, but it did
not consult any of the page-side eligibility logic. That left three
attack vectors open to anyone with a valid order key:
1. Submit a duplicate review for a product they already reviewed on
this order (stacks verified-buyer comments).
2. Submit a review for a fully-refunded line item.
3. Submit a review for a product whose owner has disabled comments.
process_rows() now mirrors the page's decision flow:
- index_eligible_order_items() runs the items through the same
woocommerce_review_order_eligible_items filter the page uses, so the
default fully-refunded filter strips refunded items from the index
(rows for them now report invalid_row).
- For each row, ItemEligibility::describe() decides STATUS_FORM /
STATUS_REVIEWED / STATUS_SKIP. REVIEWED returns the new
already_reviewed error, SKIP returns reviews_not_open. Only
STATUS_FORM proceeds to wp_insert_comment().
Three new tests cover the three rejection paths and assert no
duplicate comment is created in any of them. tearDown now also
removes the eligible-items and already_reviewed filters so test
fixtures cannot leak across cases.
* Wire ItemEligibility::describe() dispatch into the Review Order page
Calls prime() before the foreach, then per item:
- STATUS_SKIP -> skip the row entirely (reviews disabled on product)
- STATUS_REVIEWED -> render the locked variant (customer-review-order-row-reviewed.php)
- STATUS_FORM -> render the form row
* Document woocommerce_review_order_eligible_items at the handler call; soften reviewed quote border
- Add a hook docblock at the handler's apply_filters call so the
contract is discoverable from both the template and the API path.
- Drop the hardcoded rgba(0,0,0,0.1) on the reviewed-row left border;
use a plain 'border-left: 2px solid' so currentColor is honoured and
dark themes get a visible accent.
* Tighten already-reviewed lookups and the locked-row link
- ItemEligibility: get_comments() now uses status=>approve plus
include_unapproved=>[customer_email] so spam/trash reviews can't
trip STATUS_REVIEWED and re-block a legitimate retry, while the
customer's own pending review still counts. Both find_existing_review()
and prime() are updated to match.
- customer-review-order-row-reviewed.php: link to the parent product
permalink (via $item->get_product_id()) instead of the variation
permalink. Reviews are stored against the parent, so the link should
match for consistency.
- ItemEligibilityTest::test_prime_caches_results: wrap the describe()
call in try/finally so the comments_pre_query filter is always
removed even if an assertion throws.
* Fix PHPStan parameter.phpDocType: fully-qualify WC_Order_Item
The hook docblock declared @param WC_Order_Item[] without a leading
backslash, so PHPStan resolved it to
Automattic\WooCommerce\Internal\OrderReviews\WC_Order_Item which
doesn't exist. Use \WC_Order_Item[] so PHPStan picks up the global
WC class.
* Wire ItemEligibility::describe() dispatch into the Review Order page
Calls prime() before the foreach, then per item:
- STATUS_SKIP -> skip the row entirely (reviews disabled on product)
- STATUS_REVIEWED -> render the locked variant (customer-review-order-row-reviewed.php)
- STATUS_FORM -> render the form row
* Close server-side gaps in the Review Order submission handler
Previously the handler validated the row's order_item_id and that the
submitted product_id matched the line item's product_id, but it did
not consult any of the page-side eligibility logic. That left three
attack vectors open to anyone with a valid order key:
1. Submit a duplicate review for a product they already reviewed on
this order (stacks verified-buyer comments).
2. Submit a review for a fully-refunded line item.
3. Submit a review for a product whose owner has disabled comments.
process_rows() now mirrors the page's decision flow:
- index_eligible_order_items() runs the items through the same
woocommerce_review_order_eligible_items filter the page uses, so the
default fully-refunded filter strips refunded items from the index
(rows for them now report invalid_row).
- For each row, ItemEligibility::describe() decides STATUS_FORM /
STATUS_REVIEWED / STATUS_SKIP. REVIEWED returns the new
already_reviewed error, SKIP returns reviews_not_open. Only
STATUS_FORM proceeds to wp_insert_comment().
Three new tests cover the three rejection paths and assert no
duplicate comment is created in any of them. tearDown now also
removes the eligible-items and already_reviewed filters so test
fixtures cannot leak across cases.
* Add changefile(s) from automation for the following project(s): woocommerce
* Render empty-state thank-you when nothing is left to review
Pre-computes ItemEligibility decisions once, then dispatches:
- If the resulting set has no `STATUS_FORM` items, render the new empty
state template instead of the form.
- The empty state shows a thank-you heading, a short body, a summary of
reviews left on this order (count + average stars), and a "Continue
shopping" link to the shop page.
- If the customer has reviewed at least one item but the submission
handler hasn't already set `_wc_review_request_completed_at`, the
template sets it now — the empty-state visit doubles as completion
proof for late-arriving reviews from other channels.
Files:
- `templates/order/customer-review-order-empty.php` — new partial.
- `templates/order/customer-review-order.php` — pre-compute decisions,
branch on form-rows / empty-state, drop the inline `apply_filters`
loop in favour of the cached `$decisions` array.
- `client/legacy/css/order-review.scss` — empty-state card styling.
Closes #64313 (WOOPLUG-6599).
* Fix lint in empty-state thank-you template
* Add changefile(s) from automation for the following project(s): woocommerce
* Address Copilot feedback on empty-state thank-you PR
- Set the order completed-at meta whenever the empty-state renders, not
only when reviews were counted. The empty-state can be reached via
STATUS_SKIP (reviews disabled) or filter-forced STATUS_REVIEWED with
no backing comment, and back-button completion should still record
the moment.
- Switch the headline and lead paragraph based on whether any reviews
were left; rendering 'Thanks for your reviews!' with 0 reviews was
misleading.
* Move empty-state side effects out of template into Endpoint
- Endpoint::render() now stamps the no-actionable-rows meta before
delegating to the template, keeping the template purely view-side.
- Template primes the comment-meta cache via update_meta_cache() so the
per-comment rating reads no longer hit one query each.
- COMPLETED_META_KEY docblock updated to document both completion
paths (submission handler + Endpoint stamp on empty-state load).
* Wire empty-state thank-you dispatch into the Review Order page
The template now precomputes a array, falls through to the
customer-review-order-empty.php partial when no STATUS_FORM rows
remain, and uses the precomputed decisions inside the form loop so
describe() runs at most once per item.
* Drop stale 64528 changelog and tighten the empty-state wording
PR #64528 was closed without merging, so the auto-generated changelog
file that landed on this branch is irrelevant; remove it. Update the
empty-state changelog wording so it covers the no-actionable-rows
case (already-reviewed or skipped), not just already-reviewed.
* Add EndpointTest coverage for maybe_mark_no_actionable_rows
- Stamp meta when no STATUS_FORM rows remain on page load.
- Skip when at least one actionable row is present.
- Never overwrite an existing value (idempotency).
* Polish empty-state path: cleaner i18n, single-loop has_form_rows, tearDown cache reset
- customer-review-order.php: fold the has_form_rows check into the
decisions-building foreach so we don't iterate $decisions twice.
- customer-review-order-empty.php: replace the hardcoded ' (' / ')'
rating-suffix concatenation with two separate translatable
templates so locales can re-order the rating phrase freely.
- Endpoint::maybe_mark_no_actionable_rows(): drop the unused
$item->get_product() hydration; describe() handles non-product/
missing-product items via STATUS_SKIP already.
- EndpointTest::tearDown(): always call ItemEligibility::reset_cache()
so a failing assertion doesn't leak the per-request cache into
subsequent tests; remove the per-test reset_cache() calls.
* Bump template @version stamps to match the 10.9.0 release
Both templates were modified materially in this branch; bump the
@version header so theme overrides flag stale copies on update.
* Match the template's empty-state guards in the controller and dedupe
Two issues with the empty-state path:
1. maybe_mark_no_actionable_rows iterated the eligible items but didn't
skip rows where get_product() returns null. The template does (a
deleted product renders nothing). For an order whose only items are
deleted products, the page would show the empty state while
_wc_review_request_completed_at stayed unset because describe()
reports STATUS_FORM for deleted products with comments_open === true.
Mirror the template's hydration check so the controller stays
consistent with what the customer actually sees.
2. The empty-state summary counted reviewed rows, not underlying
comments. Multiple line items mapping to the same parent (variations,
quantity-split lines) all point at the same review, so a single
review showed as 'You left 2 reviews' and the rating got averaged
into itself. Track seen comment IDs so each review is counted once.
* Pin template @version stamps to 10.8.0
Earlier this branch bumped the customer-review-order.php and
customer-review-order-empty.php template headers to 10.9.0 in
anticipation of the next release. The Customer Review Request feature
ships in 10.8.0 instead, so revert both stamps.
* Add changefile(s) from automation for the following project(s): woocommerce
* Wire ItemEligibility::describe() dispatch into the Review Order page
Calls prime() before the foreach, then per item:
- STATUS_SKIP -> skip the row entirely (reviews disabled on product)
- STATUS_REVIEWED -> render the locked variant (customer-review-order-row-reviewed.php)
- STATUS_FORM -> render the form row
* Add changefile(s) from automation for the following project(s): woocommerce
* Apply the eligibility filter inside the completion check
maybe_mark_order_complete() walked raw $order->get_items() to decide
whether every product had a review. After this branch ships the refund
exclusion via woocommerce_review_order_eligible_items, refunded items
were still counted as 'missing', so a customer who reviewed every
visible row would never trip the completed-at meta from the submit
path. Run the same filter the page and submission rows use.
* Address review feedback on the AJAX submission handler
- maybe_mark_order_complete(): build the eligible-row set through the
same `woocommerce_review_order_eligible_items` filter the page uses,
count required reviews per parent product (so duplicate or
multi-variation rows need their full count of reviews), and pass
number=>0 to get_comments() so the default 20-row cap doesn't
produce false negatives on large orders.
- process_rows(): only attribute the inserted comment to a WP user
when the current request is authenticated as that user. Guests
reaching the page via the order key now get user_id=0 instead of
the order's customer id.
- customer-review-order.php: wrap the hidden `action` value in
esc_attr() for consistency with adjacent hidden inputs.
- SubmissionHandlerTest: add coverage for the `invalid_rating` and
`product_mismatch` error codes (40 tests, was 38).
* Exclude fully-refunded items from Review Order page
Adds a default callback on woocommerce_review_order_eligible_items so
the page never lists a row for a product the customer no longer owns.
A line item is considered fully refunded when the absolute refunded
quantity equals the item's quantity. Partial refunds keep the row.
* Resolve ItemEligibility from the container so it can register filters
* Address Copilot feedback on refunded-item exclusion PR
- Compare refunded vs ordered quantity as floats so fractional-quantity
line items (e.g. weight-priced products) are correctly excluded when
fully refunded.
- Update the docblock so the wording matches the implementation
('greater than or equal to' rather than 'equals').
* Resolve ItemEligibility from the container
ItemEligibility was introduced as a static utility, but a follow-up PR
(#64531) makes it register a default filter callback on
woocommerce_review_order_eligible_items. The container resolution is
where that registration is wired in, so introducing it alongside the
class itself is the natural home.
* Pre-fill the form row with the existing review instead of locking it
Reshapes WOOPLUG-6598 to match the Figma intent and customer behaviour
we want:
- No separate "locked Reviewed" row template, no badge, no
"Reviewed on X" sentence. Drops
templates/order/customer-review-order-row-reviewed.php.
- The regular form row is pre-filled with the customer's existing
rating + text when they already have a review tied to *this order*.
Customers can edit and resubmit.
- Submissions update the matching comment in place (rating meta +
content) when one exists for this order; otherwise insert as before.
Server-side lookup uses (product_id, author_email, _review_order_id)
so a tampered POST cannot edit someone else's comment.
- A review tied to a *different* order doesn't surface here, so a
repeat purchase of the same product gets a fresh empty form. The
customer's experience may legitimately differ between purchases.
- New commentmeta `_review_order_id` stamps each inserted review with
its source order. Legacy reviews without the meta are treated as
"from a different / unknown order" — they don't pre-fill or block.
- ItemEligibility drops STATUS_REVIEWED (no consumer left) and gains
prefill_for_item(). decide() returns STATUS_FORM | STATUS_SKIP plus
the existing comment (or null) for the row.
- The woocommerce_review_order_item_already_reviewed filter is gone:
the locked-row decision it gated no longer exists.
Tests: 24 across ItemEligibility + SubmissionHandler pass, including
new coverage for same-order update, previous-order non-blocking, and
legacy-meta-missing scenarios.
* Add changefile(s) from automation for the following project(s): woocommerce
* Fix branch-lint warnings: array alignment, docblock params, equals alignment
* Address PR #64530 review feedback
- Make ItemEligibility::preload_for_items idempotent per order+email so
the Endpoint + template double-call doesn't issue two identical queries
- Reset the eligibility cache at the start of SubmissionHandler::process_rows
so multi-submission tests get fresh state
- Clarify maybe_mark_no_actionable_rows docblock: scope differs from
SubmissionHandler::maybe_mark_order_complete (order-scoped vs. lifetime)
- Document that all-skip orders intentionally get stamped (matches what
the empty-state view renders)
- Wrap the completion-meta save() in try/catch + wc_get_logger, like
Scheduler logs its skips
- Dedup the inline "side effect lives in the controller" comment from
both call sites; the helper docblock now carries that explanation
- SubmissionHandler::COMPLETED_META_KEY docblock: clarify that
pending-moderation reviews also count toward completion
- EndpointTest::test_completed_meta_is_not_overwritten: drop order items
so the foreach reaches the stamping step and the guard is exercised
- Drop redundant \$customer_email re-assignment in customer-review-order.php
- Empty-state template: fall back to home_url('/') when shop page is
missing, mirroring Endpoint::gate_request
- Drop the always-true if (\$has_unreviewed_row) wrapper around the form;
the early return above guarantees it
- Changelog wording: cover the skipped-because-reviews-disabled path too
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/64530-wooplug-6599-empty-state-thank-you b/plugins/woocommerce/changelog/64530-wooplug-6599-empty-state-thank-you
index c508079871d..b7f4bc566e4 100644
--- a/plugins/woocommerce/changelog/64530-wooplug-6599-empty-state-thank-you
+++ b/plugins/woocommerce/changelog/64530-wooplug-6599-empty-state-thank-you
@@ -1,4 +1,4 @@
Significance: patch
Type: add
-Render an empty-state thank-you view on the customer Review Order page when no actionable rows remain (every eligible item is already reviewed or skipped because reviews are disabled).
\ No newline at end of file
+Render an empty-state thank-you view on the customer Review Order page when no actionable rows remain (every eligible item is already reviewed or skipped because reviews are disabled).
diff --git a/plugins/woocommerce/changelog/64531-wooplug-6600-phpunit-coverage b/plugins/woocommerce/changelog/64531-wooplug-6600-phpunit-coverage
new file mode 100644
index 00000000000..b2705624e83
--- /dev/null
+++ b/plugins/woocommerce/changelog/64531-wooplug-6600-phpunit-coverage
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Exclude fully-refunded line items from the customer Review Order page so customers can't review products they no longer own.
\ No newline at end of file
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php
index 89b3d95ea8e..ce8dd2e5ecc 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php
@@ -139,6 +139,71 @@ class ItemEligibilityTest extends WC_Unit_Test_Case {
$this->assertNull( $decision['comment'], 'Reviews from a different order must not pre-fill the current row.' );
}
+ /**
+ * @testdox exclude_fully_refunded_items drops items whose full quantity has been refunded.
+ */
+ public function test_exclude_fully_refunded_items_drops_full_refunds(): void {
+ $built = $this->make_order();
+ $order = $built['order'];
+ $item = $built['item'];
+
+ wc_create_refund(
+ array(
+ 'order_id' => $order->get_id(),
+ 'amount' => $item->get_total(),
+ 'line_items' => array(
+ $item->get_id() => array(
+ 'qty' => $item->get_quantity(),
+ 'refund_total' => $item->get_total(),
+ ),
+ ),
+ )
+ );
+
+ $fresh = wc_get_order( $order->get_id() );
+ $filtered = ItemEligibility::exclude_fully_refunded_items( $fresh->get_items(), $fresh );
+
+ $this->assertCount( 0, $filtered, 'Fully refunded line item should be excluded.' );
+ }
+
+ /**
+ * @testdox exclude_fully_refunded_items keeps partially-refunded items.
+ */
+ public function test_exclude_fully_refunded_items_keeps_partial_refunds(): void {
+ $order = OrderHelper::create_order();
+ foreach ( $order->get_items() as $line ) {
+ $order->remove_item( $line->get_id() );
+ }
+ $order->set_billing_email( 'jane@example.test' );
+ $order->set_status( OrderStatus::COMPLETED );
+
+ $product = WC_Helper_Product::create_simple_product();
+ $order->add_product( $product, 3 );
+ $order->save();
+
+ $items = $order->get_items();
+ /** @var WC_Order_Item_Product $item */
+ $item = reset( $items );
+
+ wc_create_refund(
+ array(
+ 'order_id' => $order->get_id(),
+ 'amount' => (float) $item->get_total() / 3,
+ 'line_items' => array(
+ $item->get_id() => array(
+ 'qty' => 1,
+ 'refund_total' => (float) $item->get_total() / 3,
+ ),
+ ),
+ )
+ );
+
+ $fresh = wc_get_order( $order->get_id() );
+ $filtered = ItemEligibility::exclude_fully_refunded_items( $fresh->get_items(), $fresh );
+
+ $this->assertCount( 1, $filtered, 'Partially refunded line item should still be eligible.' );
+ }
+
/**
* @testdox decide() ignores reviews without the order meta (default for legacy reviews).
*/