Skip to main content

Coupons Module

The promotional engine – create, validate, and apply discounts with strict business rules.

Coupons Admin Validation


πŸ“– Table of Contents​


Base Route : /api/v1/coupons​

All endpoints require authentication. Admin endpoints (POST /, PATCH /:id, GET /) are restricted to ADMIN role. The public endpoint /available is accessible to any authenticated user (USER or ADMIN).


Endpoints Overview​

MethodEndpointAccessDescription
POST/AdminCreate a new coupon
PATCH/:idAdminUpdate an existing coupon (toggle isActive, change limits, etc.)
GET/AdminFetch all coupons with pagination and filtering
GET/availableAuthenticated UserFetch coupons that are active, not expired, and valid for the current cart value

Rate limits: standardLimiter applies (100 requests per 15 minutes per IP). No specific coupon limiter.


Endpoint Details​

Admin Endpoints​

POST /​

Create a new promotional coupon.

Headers: Authorization: Bearer <admin_access_token>

Request body:

{
"code": "DIWALI500",
"discountType": "FLAT",
"discountValue": 500,
"minCartValue": 2500,
"startDate": "2026-05-01T00:00:00.000Z",
"expiryDate": "2026-12-31T23:59:59.000Z",
"usageLimit": 100
}

Discount types:

  • FLAT – fixed amount off (e.g., β‚Ή500 off)
  • PERCENTAGE – percentage off (max 100%)

Response (201 Created):

{
"success": true,
"statusCode": 201,
"message": "Coupon created successfully",
"data": {
"_id": "...",
"code": "DIWALI500",
"discountType": "FLAT",
"discountValue": 500,
"minCartValue": 2500,
"startDate": "2026-05-01T00:00:00.000Z",
"expiryDate": "2026-12-31T23:59:59.000Z",
"usageLimit": 100,
"usedCount": 0,
"isActive": true,
"createdAt": "...",
"updatedAt": "..."
},
"timestamp": "2026-05-22T10:30:00.000Z"
}

PATCH /:id​

Update an existing coupon. All fields are optional. Useful for:

  • Changing the discount value or minimum cart value.
  • Extending the expiry date.
  • Disabling a coupon (isActive: false) without deleting it.
  • Increasing the usageLimit.

Headers: Authorization: Bearer <admin_access_token>
URL Parameter: id – coupon ObjectId

Request body (partial):

{
"discountValue": 600,
"expiryDate": "2027-01-31T23:59:59.000Z",
"isActive": false
}

Response (200 OK): Returns the updated coupon.

To reactivate a coupon, set isActive: true. Expired coupons (expiryDate in the past) cannot be reactivated unless the date is also updated.

GET /​

Fetch all coupons with pagination and optional filtering (by isActive, discountType, etc.). Admin dashboard use only.

Headers: Authorization: Bearer <admin_access_token>
Query parameters (optional):

ParamTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page (max 100)
isActiveboolean–Filter by active/inactive
discountTypestring–FLAT or PERCENTAGE

Response (200 OK):

{
"success": true,
"statusCode": 200,
"message": "Coupons fetched successfully",
"data": {
"coupons": [ ... ],
"meta": {
"total": 50,
"limit": 20,
"page": 1
}
},
"timestamp": "2026-05-22T10:35:00.000Z"
}

Public Endpoint​

GET /available​

Fetch coupons that are active, not expired, and valid for the current cart value. The frontend uses this to show available discounts before the user applies a code.

Headers: Authorization: Bearer <user_access_token>

Query parameter:

ParamTypeRequiredDescription
cartValuenumberYesThe current cart subtotal (used to filter by minCartValue)

Example request:

GET /api/v1/coupons/available?cartValue=3000

Response (200 OK):

{
"success": true,
"statusCode": 200,
"message": "Available coupons retrieved",
"data": {
"coupons": [
{
"_id": "...",
"code": "DIWALI500",
"discountType": "FLAT",
"discountValue": 500,
"minCartValue": 2500,
"expiryDate": "2026-12-31T23:59:59.000Z"
},
{
"_id": "...",
"code": "SUMMER10",
"discountType": "PERCENTAGE",
"discountValue": 10,
"minCartValue": 1000,
"expiryDate": "2026-08-31T23:59:59.000Z"
}
]
},
"timestamp": "2026-05-22T10:40:00.000Z"
}

Only coupons with isActive: true, expiryDate in the future, and minCartValue ≀ cartValue are returned.


Validation Rules (Zod)​

EndpointFieldRules
POST /codestring, 1‑50 chars, uppercase alphanumeric + underscore, required
discountTypeenum ["FLAT", "PERCENTAGE"], required
discountValuenumber > 0, required. For PERCENTAGE: ≀ 100
minCartValuenumber β‰₯ 0, default 0
startDateISO 8601 datetime, required
expiryDateISO 8601 datetime, must be after startDate, required
usageLimitinteger β‰₯ 1, optional (default null = unlimited)
PATCH /:idAll fields optional, same rules as above
GET /availablecartValuenumber β‰₯ 0, required (coerced from query string)

All schemas use .strict() – extra fields are rejected.


Error Response​

StatusCodeExample messageWhen
400BAD_REQUEST"discountValue for PERCENTAGE cannot exceed 100"Validation fails
400BAD_REQUEST"expiryDate must be after startDate"Date logic error
401UNAUTHORIZED"Authentication required"Missing or invalid token
403FORBIDDEN"You do not have permission to perform this action"Non‑admin tries admin endpoint
404NOT_FOUND"Coupon not found"Invalid coupon ID in PATCH /:id
409CONFLICT"Coupon code already exists"Duplicate code on creation
409CONFLICT"This coupon has expired"Applying expired coupon to cart

Runbook​

For manual testing with Thunder Client / Postman, see ../../testing/coupon-runbook.md. The runbook covers:

  • Admin creating flat and percentage coupons
  • Public GET /available filtering by cart value
  • Applying coupons to cart (via cart module)
  • Edge cases: expired coupons, usage limits, min cart value


Flexible discounts, strict limits – the Reshma‑Core coupon engine.