Skip to main content

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
    });
}
  1. Validate Rating: Ensure rating is between 1-5
  2. Verify Order: Check that order exists and belongs to user
  3. Check Delivery: Only “delivered” orders can be reviewed
  4. Verify Product: Ensure product was in the order
  5. Create/Update Review: Use upsert to prevent duplicates
  6. Recalculate Ratings: Update product’s average rating and count
  7. 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" });
}
When a review is deleted:
  1. Review is removed from the database
  2. Remaining reviews for the product are fetched
  3. New average is calculated from remaining reviews
  4. If no reviews remain, averageRating is set to 0
  5. 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

  1. Customer places an order
  2. Order is delivered (status = “delivered”)
  3. Customer sees “Write a Review” option
  4. Customer submits 5-star rating with comment
  5. Review appears on product page
  6. Customer can later edit or delete their review

Business Perspective

  1. Reviews build trust with potential customers
  2. High ratings improve product visibility
  3. Feedback helps improve products and service
  4. 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

Build docs developers (and LLMs) love