Appearance
Product Reviews
Flow ID: CF-16 Module(s): eshop, Product domain Complexity: Medium Last Updated: 2026-05-07
Business Overview
Product reviews enable customers to share star ratings (1-5) and written feedback. Reviews go through an approval workflow: submitted as pending, then approved or rejected by admin. Average ratings display on product pages.
Key business behaviors:
- One review per customer per product
- Approval states: 0=pending, 1=approved, 2=rejected
- Star ratings 1-5 integer scale
- Language-scoped reviews
- Review request emails via cron job (Google, Facebook, Skroutz platforms)
API Reference
REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/product/review | Guest | List reviews (filter by productId, starPoints, active) |
| GET | /rest/product/review/{id} | Guest | Show single review |
| GET | /rest/product/review/item | Guest | Show single review (item endpoint) |
| POST | /rest/product/review | Backend | Create review |
| POST | /rest/product/review/{id} | Backend | Update (admin approval) |
| DELETE | /rest/product/review/{id} | Backend | Delete review |
Filters: id, productId, customerId, starPoints, active, lang, nickname, content
Sorts: id, productId, customerId, starPoints, reviewDate, active
Relations: product (belongs_to)
Domain Layer
| Component | Path |
|---|---|
| REST Controller | src/Rest/Product/Controllers/Review.php |
| Service | src/Domains/Product/Review/Service.php |
| WriteService | src/Domains/Product/Review/WriteService.php |
| Validator | src/Domains/Product/Review/Validator.php |
Dual path: Legacy safeAddReview() for storefront + REST for API.
Rating Aggregation
RatingAggregator (src/Domains/Product/Review/RatingAggregator.php) keeps two cached columns on shop_product in sync so product list views can render star ratings without issuing per-product review queries (eliminates N+1 for product cards).
What is cached
Column on shop_product | Type | Default | Meaning |
|---|---|---|---|
average_rating | DECIMAL(3,2) NOT NULL | 0.00 | AVG(star_points) of active reviews (active=1), rounded to 2 d.p. |
review_count | INT(10) UNSIGNED NOT NULL | 0 | COUNT(*) of active reviews (active=1) |
Recompute query
RatingAggregator::recomputeForProduct(int $productId): void runs:
sql
SELECT AVG(star_points), COUNT(*)
FROM shop_product_reviews
WHERE product_id = ? AND active = 1and writes the result back to shop_product via a CI DB update. The constructor is side-effect-free; the DB connection is lazy-loaded.
What triggers a recompute
Modern path (src/Domains/Product/Review/WriteService.php):
create()— recomputes for the new review's$entity->product_idupdate()— collects$original->product_idand$entity->product_idviaarray_unique(array_filter([...])), recomputes for each distinct product id affecteddelete()— recomputes for the deleted review's$original->product_id
Legacy path (ecommercen/eshop/models/Adv_product_reviews_model.php):
addReview()— customer storefront submissionupdateReview()— individual review editsetReviewStatus()— single-review status change (admin moderation: approve/reject)setReviewsStatusBatch()— bulk status change (admin bulk moderation)
The legacy path covers creation, update, and status flips, but NOT hard-deletes — Adv_product_reviews_model has no delete method, so a hard-delete through any legacy admin path bypasses the aggregator and leaves shop_product.average_rating and shop_product.review_count stale. Modern WriteService::delete() does invoke the aggregator.
Surfacing in the REST API
src/Rest/Product/Resources/Product/Resource.php now always includes:
averageRating(float) — maps toshop_product.average_ratingreviewCount(int) — maps toshop_product.review_count
Both fields appear in every product response (storefront and backend) so frontends can render star ratings on product cards from a single list call.
src/Domains/Product/Product/ListRequest.php exposes both as sortable (?sort=-averageRating) and filterable.
Migrations
| Migration file | Purpose |
|---|---|
database/migrations/20260428120000_add_review_aggregates_to_shop_product.php | Adds average_rating and review_count columns with indexes |
database/migrations/20260428120100_backfill_product_review_aggregates.php | One-time UPDATE JOIN backfill via Patches\BackfillProductReviewAggregates for existing reviews |
Client Extension Points
- Legacy: Override
safeAddReview()for verified-purchase check - REST Validator: Custom approval rules
- Review emails: Job
AdvSendMailToCustomersForReviewwithreviewKey(Google/Facebook/Skroutz)
Data Model
| Table | Key Columns |
|---|---|
shop_product_reviews | id, product_id, customer_id, star_points, nickname, review_date, active (0/1/2), content, lang |
shop_product | average_rating DECIMAL(3,2) NOT NULL DEFAULT 0.00, review_count INT(10) UNSIGNED NOT NULL DEFAULT 0 (both added by migration 20260428120000) |
average_rating holds the mean star_points across active reviews (active=1) rounded to 2 decimal places. review_count holds the count of the same active reviews. Both columns are indexed to support sort and filter operations. Products with no active reviews carry the default value of 0 for both columns.
Known Issues & Security Gaps
Stub Validator — no star_points range check (
src/Domains/Product/Review/Validator.php):validateForCreate()andvalidateForUpdate()initialize an empty$errors = []and return without checking anything.star_pointsrange (1–5),product_idpresence, andcustomer_idpresence are not enforced at the REST layer. A caller can create a review withstar_points = 0, a negative value, or missing foreign keys.Legacy hard-delete bypasses RatingAggregator (
ecommercen/eshop/models/Adv_product_reviews_model.php): this model has no delete method. A hard-delete of a review through any legacy admin path leavesshop_product.average_ratingandshop_product.review_countstale becauserecomputeRatingAggregates()is never called.Stale RatingAggregator docblock (
src/Domains/Product/Review/RatingAggregator.php:11): the class comment previously claimed "Legacy: Adv_product_reviews_model on add/update/delete" — this is inaccurate (no legacy delete path exists). The docblock has been corrected to remove "delete" but the gap in coverage remains (see item 2 above).
Related Flows
- CF-02 Product Detail — reviews on PDP
- CF-11 Customer Account — customer can view their submitted reviews
- CF-32 Loyalty Points — approved reviews can award loyalty points (
CUSTOMER_REVIEW_REWARD) - SY-11 Review Reminders — review request cron job (Google/Facebook/Skroutz emails)
- AD-52 Review Moderation — admin-side review approval, rejection, and reply workflow