Overview
Don Palito Jr includes a product review system that allows customers to rate and review products from their delivered orders. Reviews automatically update product average ratings and help other customers make informed purchasing decisions.
Review Schema
Reviews are linked to products, users, and orders for comprehensive tracking:
const reviewSchema = new mongoose . Schema ({
productId: {
type: mongoose . Schema . Types . ObjectId ,
ref: "Product" ,
required: true
},
userId: {
type: mongoose . Schema . Types . ObjectId ,
ref: "User" ,
required: true
},
orderId: {
type: mongoose . Schema . Types . ObjectId ,
ref: "Order" ,
required: true
},
rating: {
type: Number ,
required: true ,
min: 1 ,
max: 5
},
comment: {
type: String ,
trim: true ,
maxlength: 500 ,
default: '' ,
},
},
{
timestamps: true
});
Reviews require three IDs (product, user, order) to ensure only verified purchasers can leave reviews for products they actually received.
Review Features
Verified Purchases Only customers who received a product can review it.
5-Star Rating Standard 1-5 star rating system with optional text comment.
Auto-Update Ratings Product average ratings are automatically recalculated when reviews are added or deleted.
Order-Based Reviews are tied to specific orders, preventing duplicate reviews.
Creating Reviews
Customers can submit reviews for products in their delivered orders:
export async function createReview ( req , res ) {
const { productId , orderId , rating } = req . body ;
if ( ! rating || rating < 1 || rating > 5 ) {
return res . status ( 400 ). json ({ error: "Rating must be between 1 and 5" });
}
const user = req . user ;
const order = await Order . findById ( orderId );
// verify order exists and is delivered
if ( ! order ) {
return res . status ( 404 ). json ({ error: "Order not found" });
}
if ( order . clerkId !== user . clerkId ) {
return res . status ( 403 ). json ({ error: "Not authorized to review this order" });
}
if ( order . status !== "delivered" ) {
return res . status ( 400 ). json ({ error: "Can only review delivered orders" });
}
// verify product is in the order
const productInOrder = order . orderItems . find (
( item ) => item . product . toString () === productId . toString ()
);
if ( ! productInOrder ) {
return res . status ( 400 ). json ({ error: "Product not found in this order" });
}
// atomic update or create
const review = await Review . findOneAndUpdate (
{ productId , userId: user . _id , orderId },
{ rating , orderId , productId , userId: user . _id },
{ new: true , upsert: true , runValidators: true }
);
// update the product rating with atomic aggregation
const reviews = await Review . find ({ productId });
const totalRating = reviews . reduce (( sum , rev ) => sum + rev . rating , 0 );
const updatedProduct = await Product . findByIdAndUpdate (
productId ,
{
averageRating: totalRating / reviews . length ,
totalReviews: reviews . length ,
},
{ new: true , runValidators: true }
);
if ( ! updatedProduct ) {
await Review . findByIdAndDelete ( review . _id );
return res . status ( 404 ). json ({ error: "Product not found" });
}
return res . status ( 201 ). json ({
message: "Review submitted successfully" ,
review
});
}
Validate Rating : Ensure rating is between 1-5
Verify Order : Check that order exists and belongs to user
Check Delivery : Only “delivered” orders can be reviewed
Verify Product : Ensure product was in the order
Create/Update Review : Use upsert to prevent duplicates
Recalculate Ratings : Update product’s average rating and count
Rollback on Failure : Delete review if product update fails
The system uses findOneAndUpdate with upsert: true to allow customers to update their existing review instead of creating duplicates.
Review Verification
Multiple checks ensure review authenticity:
Order Ownership Verification
if ( order . clerkId !== user . clerkId ) {
return res . status ( 403 ). json ({ error: "Not authorized to review this order" });
}
Delivery Status Check
if ( order . status !== "delivered" ) {
return res . status ( 400 ). json ({ error: "Can only review delivered orders" });
}
Product in Order Verification
const productInOrder = order . orderItems . find (
( item ) => item . product . toString () === productId . toString ()
);
if ( ! productInOrder ) {
return res . status ( 400 ). json ({ error: "Product not found in this order" });
}
These checks ensure that only verified customers who actually received a product can leave reviews, maintaining review credibility.
Automatic Rating Calculation
When a review is created or deleted, product ratings are automatically updated:
const reviews = await Review . find ({ productId });
const totalRating = reviews . reduce (( sum , rev ) => sum + rev . rating , 0 );
await Product . findByIdAndUpdate (
productId ,
{
averageRating: totalRating / reviews . length ,
totalReviews: reviews . length ,
},
{ new: true , runValidators: true }
);
Product Rating Fields
The product schema stores calculated values:
averageRating : {
type : Number ,
min : 0 ,
max : 5 ,
default : 0
},
totalReviews : {
type : Number ,
min : 0 ,
default : 0
}
Storing calculated averages on the product allows fast display without recalculating on every product fetch. The values are automatically kept in sync when reviews change.
Deleting Reviews
Customers can delete their own reviews:
export async function deleteReview ( req , res ) {
const { reviewId } = req . params ;
const user = req . user ;
const review = await Review . findById ( reviewId );
if ( ! review ) {
return res . status ( 404 ). json ({ error: "Review not found" });
}
if ( review . userId . toString () !== user . _id . toString ()) {
return res . status ( 403 ). json ({ error: "Not authorized to delete this review" });
}
const productId = review . productId ;
await Review . findByIdAndDelete ( reviewId );
// Recalculate product rating
const reviews = await Review . find ({ productId });
const totalRating = reviews . reduce (( sum , rev ) => sum + rev . rating , 0 );
await Product . findByIdAndUpdate ( productId , {
averageRating: reviews . length > 0 ? totalRating / reviews . length : 0 ,
totalReviews: reviews . length ,
});
return res . status ( 200 ). json ({ message: "Review deleted successfully" });
}
Rating Update on Deletion
When a review is deleted:
Review is removed from the database
Remaining reviews for the product are fetched
New average is calculated from remaining reviews
If no reviews remain, averageRating is set to 0
totalReviews count is updated
Review Status in Orders
The order listing includes review status to help users know which orders they can review:
export async function getUserOrders ( req , res ) {
const orders = await Order . find ({ clerkId: req . user . clerkId })
. populate ( "orderItems.product" , "name images price" )
. sort ({ createdAt: - 1 });
const orderIds = orders . map (( order ) => order . _id );
const reviews = await Review . find ({ orderId: { $in: orderIds } });
const reviewedOrderIds = new Set ( reviews . map (( review ) => review . orderId . toString ()));
const ordersWithReviewStatus = orders . map (( order ) => {
// ...
return {
... orderObj ,
orderItems: orderItemsWithNames ,
hasReviewed: reviewedOrderIds . has ( order . _id . toString ()),
};
});
return res . status ( 200 ). json ({ orders: ordersWithReviewStatus });
}
The hasReviewed flag helps the frontend display “Write a Review” buttons only for orders that haven’t been reviewed yet.
Review Display
Products display their aggregated rating information:
{
"_id" : "abc123" ,
"name" : "Delicious Empanada" ,
"price" : 5000 ,
"averageRating" : 4.5 ,
"totalReviews" : 24 ,
// ... other fields
}
This allows frontends to show:
⭐⭐⭐⭐⭐ 4.5/5 (24 reviews)
Star ratings on product cards
Review counts for social proof
Rating Constraints
Minimum Rating Ratings must be at least 1 star.
Maximum Rating Ratings cannot exceed 5 stars.
Comment Length Optional comments are limited to 500 characters.
One Review Per Order Each customer can leave one review per product per order.
Use Cases
Customer Perspective
Customer places an order
Order is delivered (status = “delivered”)
Customer sees “Write a Review” option
Customer submits 5-star rating with comment
Review appears on product page
Customer can later edit or delete their review
Business Perspective
Reviews build trust with potential customers
High ratings improve product visibility
Feedback helps improve products and service
Verified purchase badges increase credibility
Consider displaying reviews prominently on product pages and highlighting highly-rated products to drive conversions.
Review Model: backend/src/models/review.model.js:1-38
Review Controller: backend/src/controllers/review.controller.js:5-105
Create Review: Lines 5-72
Delete Review: Lines 74-105
Order with Review Status: backend/src/controllers/order.controller.js:77-108