Review Module Backend
Architecture, data model, and backend invariants for the review module.
Summary
The review module now has two distinct persistence models:
order_reviewsfor product reviews, including both customer-submitted reviews and admin-seeded fake-persona reviewstestimonialsfor admin-created storefront social proof
Testimonials remain separate. Admin-seeded product reviews now reuse order_reviews because they are intended to appear inside the actual product review stream and affect the same rating aggregates.
Data Model
order_reviews
Purpose:
- customer-submitted reviews for delivered order items
- admin-seeded reviews with explicit fake reviewer persona fields
- admin moderation before storefront display
Key fields:
orderIdproductIdcustomerIdreviewSourcereviewerNamereviewerImageUrlratingcommentstatusadminRemarksreviewedByAdminIdreviewedAt- timestamps
Key invariants:
- customer reviews are unique per
customerId + orderId + productId - rating must be between
1and5 reviewSource = 'customer'requires a realcustomerIdreviewSource = 'admin_seeded'requirescustomerId = nulland a non-blankreviewerName- pending reviews have no admin review metadata
- approved/rejected reviews require admin review metadata
testimonials
Purpose:
- admin-authored trust content
- public storefront list not tied to product ownership
Key fields:
nameratingcommentimageUrlisVisibleorder- timestamps
Key invariants:
- rating must be between
1and5 nameandcommentmust remain non-blank after trimming- visibility drives storefront exposure
ordersupports merchandising order independent of create time
Module Layout
Admin side
- existing review admin controller/service still own order review moderation
- new testimonial admin controller/service own testimonial CRUD
- both stay under the review admin module and reuse
Reviews_*permissions
Public side
- existing review public controller/service still own product page review reads
- new testimonial public controller/service own storefront testimonial reads
Mobile side
- existing authenticated mobile controller still owns customer-submitted product reviews
- existing mobile public product-review controller remains unchanged
- new mobile testimonial controller exposes storefront testimonials under mobile route prefix
Query Behavior
Admin testimonial list
Filters:
searchisVisible
Sort fields:
ordercreatedAtupdatedAtrating
Ordering:
- primary requested field
- secondary
createdAt descfor stable output
Public testimonial list
Filters:
- visible only
- optional exact
rating
Sort fields:
ordercreatedAtrating
Ordering:
- primary requested field
- secondary
createdAt desc
Why Testimonials Stay Separate
Trying to reuse order_reviews for testimonials would force one of these bad tradeoffs:
- make
orderIdandproductIdnullable and loosen existing purchase rules - invent fake orders/products just to store admin-authored content
- overload existing controllers and DTOs with branching logic
The separate testimonials table keeps:
- purchased product reviews trustworthy
- testimonial content simple
- API docs easy to understand
Admin-seeded fake product reviews are different from testimonials:
- they are attached to a specific
productId - they participate in the same approved-review totals and averages
- they appear through the same product review endpoints as customer reviews
That is why the product-review stream now supports two row modes inside order_reviews instead of creating a third table.
Product Review Discovery
Product reviews are surfaced on product detail pages through product-specific endpoints:
GET /api/products/:slug/reviews— public: approved reviews for a product by its slugGET /api/products/id/:productId/reviews— public: approved reviews by product IDGET /api/mobile/products/id/:productId/reviews— public mobile variant
The review-to-product linkage is through order_reviews.product_id, which is a FK to product.id. Only approved reviews are returned in public responses. Review count and average rating may be cached alongside product detail data.
Discovery Flow
- Customer views product detail (
GET /api/products/:slug) - Customer requests product reviews (
GET /api/products/:slug/reviews) for social proof - System returns approved reviews with pagination, filtered by product slug resolution
- Customer sees star ratings, comments, and review metadata on the product page
Integration Notes
- permissions already exist under
Reviews_*, so no new permission family is needed - default Swagger must include both review admin and public modules so testimonial routes appear in generated docs
- mobile docs inherit testimonial routes through
ReviewMobileModule
Product Average Rating Integration
Average ratings are computed on the fly from the order_reviews table using SQL aggregation. There is no separate summary table; every product list and detail query derives averageRating and reviewCount from approved reviews in real time.
Computation Strategy
The module uses two complementary patterns depending on the query context:
| Context | Method | How it works |
|---|---|---|
| Product list (findAll) | Correlated SQL subquery | SELECT AVG(r.rating) FROM order_reviews r WHERE r.product_id = products.id AND r.status = 'approved' embedded in the main product SELECT |
| Product detail (findBySlugForCustomer) | getApprovedReviewSummary() | Separate query runs AVG(rating) and COUNT(*) on order_reviews for a single productId with status = 'approved' |
Fields
| Field | Type | Location | Description |
|---|---|---|---|
averageRating | number | null (list) / number (detail) | ProductListItemDto / ProductResponseDto | Mean rating of approved reviews, rounded to 2 decimal places. Null in list when no approved reviews exist. |
reviewCount | number (list) | ProductListItemDto | Count of approved reviews for the product. |
totalReviews | number (detail) | ProductResponseDto | Same as reviewCount, under the detail DTO field name. |
reviewStatus | string (detail) | ProductResponseDto | Human-readable label: "Reviews available" when totalReviews > 0, otherwise "No reviews yet". |
Where the computation lives
ProductCustomerService.findAll()— correlated subquery in each product list variant (all products, featured products, new arrivals, related products). The subquery filters byproduct_idandstatus = 'approved'.ProductCustomerService.findBySlugForCustomer()— callsgetApprovedReviewSummary(productId)to fetch aggregate data for the product detail page.ProductAdminService.findAll()— uses the same correlated subquery pattern for the admin list view.ReviewPublicService.findApprovedByProductSlug()— computes summary alongside the paginated review list usingAVG()andCOUNT()for the response aggregate.
Consistency
Because the average is computed from the source table rather than cached in a summary table, the value is always consistent with the current set of approved reviews:
- Creating a new approved review immediately changes the average for subsequent queries.
- Changing a review status from
approvedtorejected(or vice versa) is reflected in the next query. - Deleting a review recalculates the average on the next read.
- No staleness window, no reconciliation job needed, no cache invalidation coordination.
Structured data (JSON-LD)
The product detail endpoint embeds the review summary as AggregateRating structured data:
{
"@type": "AggregateRating",
"ratingValue": "4.50",
"reviewCount": 12
}This is conditionally included when totalReviews > 0 and attached to the Product schema.org entity in the product detail response.
Performance notes
- The correlated subquery adds a per-row aggregation cost on product list queries. Indexes on
order_reviews.product_idandorder_reviews.statusmitigate this. Both columns are covered by existing composite query patterns. - For detail views the cost is negligible since it's a single product with a filtered aggregation.
- If list query performance becomes a concern, a materialized summary table (
product_review_summary) can be introduced as a future optimisation without changing the API contract.
See also
- Product Backend: See Product Customer Service for the product list and detail query architecture.
- API Reference: See Review Module API section "Review Summary Integration" for the response shape changes.
Admin Review Creation
Admin users can seed fake product reviews directly through POST /api/admin/reviews.
Key Rules
- Admin-created seeded reviews use
order_reviewswithreviewSource = 'admin_seeded'. customerIdis null for seeded reviews.reviewerNameis required and is shown publicly exactly as entered.reviewerImageUrlis optional and is used anywhere the public review DTO already supports images.- The
orderIdcan be null for seeded reviews. - Seeded reviews are inserted as
approved, so they immediately affect public product review reads and rating aggregates. - Customer-submitted reviews still follow the purchased-order validation path and store
reviewSource = 'customer'.