Skip to main content

Overview

Don Palito Jr includes a comprehensive coupon system that allows administrators to create discount codes for customers. Coupons support both percentage and fixed-amount discounts, with optional expiration dates and per-user usage tracking.

Coupon Schema

Coupons are stored in MongoDB with validation and tracking features:
const couponSchema = new mongoose.Schema(
  {
    code: {
      type: String,
      required: true,
      unique: true,
      uppercase: true,
      trim: true,
    },

    discountType: {
      type: String,
      enum: ["percentage", "fixed"],
      required: true,
    },

    discountValue: {
      type: Number,
      required: true,
      min: 1,
    },

    expiresAt: {
      type: Date,
      default: null,
    },

    isActive: {
      type: Boolean,
      default: true,
    },

    usedBy: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: "User",
      },
    ],
  },
  { timestamps: true }
);
Coupon codes are automatically converted to uppercase and trimmed to ensure consistent lookups (e.g., “summer20” becomes “SUMMER20”).

Coupon Types

Percentage Discount

Reduces the order subtotal by a percentage (e.g., 20% off). Must be between 1-100.

Fixed Discount

Reduces the order subtotal by a fixed amount (e.g., $5,000 COP off). Cannot exceed subtotal.

Creating Coupons

Administrators can create new discount coupons:
export const createCoupon = async (req, res) => {
    const { code, discountType, discountValue, expiresAt } = req.body;

    if (!code || !discountType || !discountValue) {
        return res.status(400).json({ error: "code, discountType y discountValue son requeridos." });
    }

    if (!["percentage", "fixed"].includes(discountType)) {
        return res.status(400).json({ error: "discountType debe ser 'percentage' o 'fixed'." });
    }

    if (discountType === "percentage" && (discountValue < 1 || discountValue > 100)) {
        return res.status(400).json({ error: "El porcentaje debe estar entre 1 y 100." });
    }

    const existing = await Coupon.findOne({ code: code.toUpperCase().trim() });
    if (existing) {
        return res.status(409).json({ error: "Ya existe un cupón con ese código." });
    }

    const coupon = await Coupon.create({
        code,
        discountType,
        discountValue,
        expiresAt: expiresAt || null,
    });

    return res.status(201).json({ coupon });
};
  • Code must be unique (checked against existing coupons)
  • Discount type must be either “percentage” or “fixed”
  • Percentage discounts must be between 1-100
  • Expiration date is optional (null = never expires)

Validating Coupons

Before applying a coupon at checkout, the system validates it:
export const validateCoupon = async (req, res) => {
    const { code, subtotal } = req.body;

    if (!code || !subtotal) {
        return res.status(400).json({ error: "El código y el subtotal son requeridos." });
    }

    const coupon = await Coupon.findOne({ code: code.toUpperCase().trim() });

    if (!coupon) {
        return res.status(404).json({ error: "El cupón no existe o ya expiró." });
    }

    if (!coupon.isActive) {
        return res.status(400).json({ error: "El cupón no está activo." });
    }

    if (coupon.expiresAt && new Date() > coupon.expiresAt) {
        return res.status(400).json({ error: "El cupón ha expirado." });
    }

    const userId = req.user._id;
    const alreadyUsed = coupon.usedBy.some(
        (id) => id.toString() === userId.toString()
    );

    if (alreadyUsed) {
        return res.status(400).json({ error: "Ya usaste este cupón anteriormente." });
    }

    let discountAmount = 0;
    if (coupon.discountType === "percentage") {
        discountAmount = Math.min(
            Math.round((subtotal * coupon.discountValue) / 100),
            subtotal
        );
    } else {
        discountAmount = Math.min(coupon.discountValue, subtotal);
    }

    return res.status(200).json({
        coupon: {
            code: coupon.code,
            discountType: coupon.discountType,
            discountValue: coupon.discountValue,
        },
        discountAmount,
    });
};
Coupons can only be used once per user. The system tracks usage in the usedBy array to prevent reuse.

Validation Checks

A coupon is considered valid only if:
  1. ✅ The coupon exists in the database
  2. ✅ The coupon is active (isActive: true)
  3. ✅ The coupon hasn’t expired (or has no expiration date)
  4. ✅ The user hasn’t used it before

Calculating Discounts

Percentage Discounts

if (coupon.discountType === "percentage") {
    discount = Math.round((subtotal * coupon.discountValue) / 100);
}
Example: 20% off a 50,000COPorder=50,000 COP order = 10,000 COP discount

Fixed Discounts

else {
    discount = Math.min(coupon.discountValue, subtotal);
}
Example: 5,000COPoffa5,000 COP off a 50,000 COP order = $5,000 COP discount
Fixed discounts are capped at the subtotal amount to prevent negative order totals.

Applying Coupons at Checkout

During payment processing, valid coupons are automatically applied:
if (couponCode) {
    const coupon = await Coupon.findOne({ code: couponCode.toUpperCase().trim() });

    const isValid =
        coupon &&
        coupon.isActive &&
        (!coupon.expiresAt || new Date() < coupon.expiresAt);

    if (!isValid) {
        return res.status(400).json({ error: "El cupón no es válido o ha expirado." });
    }

    const alreadyUsed = coupon.usedBy.some(
        (id) => id.toString() === user._id.toString()
    );
    if (alreadyUsed) {
        return res.status(400).json({ error: "Ya usaste este cupón anteriormente." });
    }
    
    if (coupon.discountType === "percentage") {
        discount = Math.round((subtotal * coupon.discountValue) / 100);
    } else {
        discount = Math.min(coupon.discountValue, subtotal);
    }

    appliedCoupon = coupon;
}

const shipping = 10000;
const total = subtotal + shipping - discount;
After successful payment (either via Stripe webhook or bank transfer order creation), the coupon is marked as used:
if (couponCode) {
    await Coupon.findOneAndUpdate(
        { code: couponCode },
        { $addToSet: { usedBy: userId } } 
    );
}
The $addToSet operator ensures the user ID is added only once, even if the webhook fires multiple times.

Managing Coupons

Listing All Coupons (Admin)

export const getCoupons = async (req, res) => {
    const coupons = await Coupon.find().sort({ createdAt: -1 });
    return res.status(200).json({ coupons });
};

Listing Active Coupons (Public)

export const getActiveCoupons = async (req, res) => {
    const now = new Date();
    const coupons = await Coupon.find({
        isActive: true,
        $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }],
    }).select("code discountType discountValue expiresAt firstOrderOnly").lean();

    const result = coupons.map((c) => ({
        code: c.code,
        discountType: c.discountType,
        discountValue: c.discountValue,
        firstOrderOnly: c.firstOrderOnly ?? false,
        expiresAt: c.expiresAt,
    }));

    return res.status(200).json({ coupons: result });
};
Active coupons are those that are enabled (isActive: true) and haven’t expired. This endpoint can be used to display available promotions to customers.

Updating Coupons

export const updateCoupon = async (req, res) => {
    const { id } = req.params;
    const updates = req.body;

    if (updates.code) {
        updates.code = updates.code.toUpperCase().trim();
    }

    const discountType = updates.discountType;
    const discountValue = updates.discountValue;
    if (discountValue !== undefined) {
        const existingCoupon = discountType === undefined
            ? await Coupon.findById(id).select("discountType").lean()
            : null;
        const effectiveType = discountType ?? existingCoupon?.discountType;
        if (effectiveType === "percentage" && (discountValue < 1 || discountValue > 100)) {
            return res.status(400).json({ error: "El porcentaje debe estar entre 1 y 100." });
        }
    }

    const coupon = await Coupon.findByIdAndUpdate(id, updates, {
        new: true,
        runValidators: true,
    });

    if (!coupon) {
        return res.status(404).json({ error: "Cupón no encontrado." });
    }

    return res.status(200).json({ coupon });
};

Deleting Coupons

export const deleteCoupon = async (req, res) => {
    const { id } = req.params;

    const coupon = await Coupon.findByIdAndDelete(id);

    if (!coupon) {
        return res.status(404).json({ error: "Cupón no encontrado." });
    }

    return res.status(200).json({ message: "Cupón eliminado correctamente." });
};
Instead of deleting coupons that are no longer valid, consider setting isActive: false to maintain historical records while preventing new usage.

Usage Tracking

The usedBy array tracks which users have used each coupon:
usedBy: [
  {
    type: mongoose.Schema.Types.ObjectId,
    ref: "User",
  },
]
This allows:
  • One-time use per customer: Prevents coupon abuse
  • Usage analytics: Track how many customers used each coupon
  • Targeted promotions: Identify customers who haven’t used specific coupons
  • Coupon Model: backend/src/models/coupon.model.js:1-50
  • Coupon Controller: backend/src/controllers/coupon.controller.js:1-190
  • Validate Coupon: Lines 3-68
  • Create Coupon: Lines 80-113
  • Update Coupon: Lines 115-150
  • Delete Coupon: Lines 152-167
  • Get Active Coupons: Lines 169-190

Build docs developers (and LLMs) love