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 });
};
Coupon Creation Validations
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:
✅ The coupon exists in the database
✅ The coupon is active (isActive: true)
✅ The coupon hasn’t expired (or has no expiration date)
✅ 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 , 000 C O P o r d e r = 50,000 COP order = 50 , 000 COP or d er = 10,000 COP discount
Fixed Discounts
else {
discount = Math . min ( coupon . discountValue , subtotal );
}
Example: 5 , 000 C O P o f f a 5,000 COP off a 5 , 000 COP o ff 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 ;
When is a Coupon Marked as Used?
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