Promotion Module Feature List
Promotion admin/customer features, eligibility model, state behavior, and cache/rate-limit contracts.
Promotion Module - Feature List
1. Feature Overview
Promotion module manages coupon lifecycle and customer coupon operations.
Promotions support three cart type eligibility modes ("digital", "physical", "both") and three payment method eligibility modes ("online", "cod", "any"). Eligibility is enforced at coupon apply time and re-validated at order creation for payment method conflicts.
2. Route Ownership and Surfaces
| Surface | Route prefix | Owner |
|---|---|---|
| Admin API | /api/admin/promotions | PromotionAdminController |
| Customer API | /api/promotions | PromotionCustomerController |
| Mobile-composed customer API | /api/mobile/promotions | MobileModule composition |
3. Admin Feature Matrix
| Capability | Endpoint | AuthZ |
|---|---|---|
| List promotions | GET /api/admin/promotions | Promotions_READ |
| Promotion detail | GET /api/admin/promotions/:id | Promotions_READ |
| Create promotion | POST /api/admin/promotions | Promotions_CREATE |
| Update promotion | PATCH /api/admin/promotions/:id | Promotions_UPDATE |
| Delete promotion | DELETE /api/admin/promotions/:id | Promotions_DELETE |
| Disable promotion | POST /api/admin/promotions/:id/disable | Promotions_UPDATE |
4. Customer/Mobile Feature Matrix
| Capability | Endpoint | Runtime mode |
|---|---|---|
| Apply coupon to active cart | POST /api/promotions/apply (without productId) | validate + mutate cart |
| Validate coupon for product | POST /api/promotions/apply (with productId) | validate only |
| Remove coupon from cart | POST /api/promotions/remove | mutate cart |
| List active promotions | GET /api/promotions | read |
| List customer usage history | GET /api/promotions/my-usage | read |
Mobile equivalents are available under /api/mobile/promotions/....
5. Key Business Rules
- coupon input is normalized (
trim + uppercase) before lookup - promotions enforce date-window validity and discount constraints
- scope targeting uses typed nullable FKs (
product_id,category_id,tag_id) with exactly one target per scope row - usage rows require at least one redemption anchor (
cart_idororder_id) - unresolved invalid legacy usage rows are rejected (no silent drift)
- Partial discount on mixed carts: When
cartTypeEligibilityis"digital"or"physical"(not"both"), the discount is computed oneligibleSubtotal— the sum ofunitSp × quantityfor items whoseproductKindmatches the coupon's cart type. Physical items are excluded from digital-coupon discounts and vice versa. minCartValueuses eligible subtotal: The minimum cart value threshold is checked againsteligibleSubtotal, not the fullcart.totalSp. A mixed cart with total 1000 but only 400 in eligible items fails aminCartValue = 500coupon.care_packagetreated as digital: Products withproductKind = "care_package"are treated identically to digital products for all eligibility checks.- COD coupon requires physical cart: A coupon with
paymentMethodEligibility = "cod"is only valid for carts containing only physical products (not"mixed","digital", or"care_package"carts). Fails withCART_TYPE_MISMATCH_ERRORat apply time. - COD payment re-validation at order creation: If a COD coupon is applied but the order is placed with an online payment (or vice versa), the order creation is rejected with
ORDER_COUPON_PAYMENT_METHOD_MISMATCH. "both"applies to all cart kinds: Discount is computed on the fullcart.totalSp.
6. State Models
6.1 Promotion lifecycle state
6.2 Coupon operation state
6.3 Apply coupon paths
POST /promotions/apply has two valid paths:
- cart path (
productIdomitted): coupon is validated and cart totals are updated - product validation path (
productIdprovided): returns validation payload and does not mutate cart
7. Caching Strategy
Keyspaces:
promotions:admin:list:promotions:admin:detail:promotions:customer:list:promotions:code:
Behavior:
- deterministic key construction via
CacheKeyUtil.build(...) - prefix invalidation after admin mutations (
invalidatePattern(prefix*)) - customer reads reuse cached code + active-list payloads
8. Rate Limiting
Promotion rate-limit envs:
PROMOTION_ADMIN_RATE_LIMITPROMOTION_ADMIN_RATE_WINDOW_SECONDSPROMOTION_CUSTOMER_OPERATION_RATE_LIMITPROMOTION_CUSTOMER_RATE_WINDOW_SECONDS
Runtime behavior:
- Redis sorted-set sliding windows per action + actor key
- UUIDv7 request markers in rate-limit windows
- threshold breach throws
RateLimitExceededException(RATE_LIMIT_EXCEEDED)
9. Queue/Worker Features
Promotion module does not own BullMQ queues or background workers.
Operational notes:
- Coupon validation and application happen synchronously in request flow.
- There is no promotion-specific async job contract in
packages/jobsfor this module.
10. Error UX Mapping
| Scenario | API behavior | Recommended UI |
|---|---|---|
| Promotion not found | 404 PROMOTION_NOT_FOUND | Show "Coupon not found" toast |
| Code already exists | 409 PROMOTION_CODE_EXISTS | Show "Code already exists" error |
| Not applicable to cart | 400 PROMOTION_NOT_APPLICABLE | Show "Coupon not valid for this cart" |
| Invalid date range | 400 PROMOTION_INVALID_DATE_RANGE | Show "Invalid coupon dates" |
| Invalid discount | 400 PROMOTION_DISCOUNT_INVALID | Show discount validation error |
| Invalid scope | 400 PROMOTION_SCOPE_INVALID | Show "Invalid coupon targets" |
| Usage conflict | 400 PROMOTION_USAGE_CONFLICT | Show "Cannot modify used coupon" |
| Payment method mismatch at order creation | 400 ORDER_COUPON_PAYMENT_METHOD_MISMATCH | Show "Coupon not valid for selected payment method" |
| Rate limit exceeded | 429 RATE_LIMIT_EXCEEDED | Show "Please wait" with backoff |
11. Data Flow
12. Release/QA Checklist
- Admin CRUD/disable routes enforce
Promotions_*permissions. -
/promotions/applysupports both cart-apply and product-validate modes. - Scope target invariant (
product/category/tag) allows exactly one target per row. - Usage target invariant (
cart_idororder_id) is enforced. - Promotion code uniqueness conflicts return
PROMOTION_CODE_EXISTS. - Admin write paths invalidate admin/detail/code cache prefixes.
- Customer/admin operation limits enforce configured window thresholds.
- Physical coupon applied to physical cart → discount applied correctly
- Digital coupon applied to mixed cart → partial discount (digital items only)
- Physical coupon applied to digital cart → rejected (CART_TYPE_MISMATCH_ERROR)
- COD coupon applied to digital or mixed cart → rejected at apply time (CART_TYPE_MISMATCH_ERROR)
- COD coupon + COD payment method at order creation → passes
- Online coupon + COD payment at order creation → rejected (ORDER_COUPON_PAYMENT_METHOD_MISMATCH)
- "both" coupon on any non-empty cart → discount on full cart.totalSp
- "any" coupon with any payment method → passes
- minCartValue checked against eligibleSubtotal (not cart.totalSp) for non-"both" coupons
13. Integration Flows
13.1 Apply Coupon Flow
A customer applies a coupon code to their cart or validates it for a product.
- Customer calls
POST /api/promotions/apply(orPOST /api/mobile/promotions/apply) with{ code: "SAVE20" }. - System normalizes code:
trim() + uppercase()to "SAVE20". - System looks up promotion by normalized code.
- System validates promotion exists, is active, and current date is within
startDateandendDate. - System validates discount constraints: min purchase amount, max discount cap.
- If
productIdprovided: return validation payload only, do not mutate cart. - If no
productId: system validates cart contents:isCartTypeEligible(cart, promotion.cartTypeEligibility). If the cart kind does not qualify for the coupon's cart type requirement, returnCART_TYPE_MISMATCH_ERROR. Then check payment method:isPaymentMethodEligible(cart.cartKind, promotion.paymentMethodEligibility). If mismatch, returnCART_TYPE_MISMATCH_ERROR. - System evaluates scope targeting: checks if cart products match any promotion scope (product, category, or tag).
- If applicable: system updates cart's
appliedPromotionId,appliedPromotionCode,appliedDiscount. - System invalidates cart cache
cart:user:{userId}and promotion code cache. - Customer fetches updated cart to see discounted total.
13.2 Admin Create Promotion Flow
An admin creates a new promotion coupon with targeting and constraints.
- Admin calls
POST /api/admin/promotionswith promotion details. - System validates: code uniqueness (returns 409 if duplicate), date range, discount value.
- System creates promotion record with status
active(or schedules activation). - For each scope target (product, category, or tag), system inserts
promotion_scoperow with exactly one target. - System caches promotion under
promotions:admin:detail:{id}andpromotions:code:{normalizedCode}. - Admin can later call
POST /api/admin/promotions/:id/disableto deactivate the promotion. - Active promotions appear in customer
GET /api/promotionslist. - Customer usage creates
promotion_usagerows linking tocart_idororder_id.
Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.
See Also
- API Reference: See Promotion - API & Integration Guide Section 7 (Endpoint Reference + Payload Cheatsheet) for complete request/response DTOs.
- Backend Reference: See Promotion - Backend Documentation Section 3 (Data Model) for schema details.