6.7 KiB
Coupon & Promotion Code Validation Updates
Date: February 2, 2026
Status: ✅ Complete
Summary
Updated coupon and promotion code validation logic in resolveCouponCode() to:
- Fix direct coupon ID resolution flow (retrieve coupon first to avoid Stripe exceptions)
- Clarify that customer restrictions are only available in promotion code objects
- Add new error constant
PROMO_INVALID_COUPONfor all coupon/promo validation errors - Optimize expansion strategy to reuse expanded data and avoid redundant API calls
Changes
1. Code Changes
helpers/constants.js
- Added:
PROMO_INVALID_COUPON: 'promo_invalid_coupon'error constant - Location: Promo error codes section (after
PROMO_COUPON_NOT_FOUND) - Purpose: Consistent error type for all coupon/promotion code validation failures
controllers/subscription.js - resolveCouponCode()
Fixed Resolution Flow:
// STEP 1: Try as promotion code
stripe.promotionCodes.list({
code: code,
expand: ['data.promotion.coupon', 'data.promotion.coupon.applies_to']
});
// STEP 2: Try as direct coupon ID
// NEW: Retrieve coupon FIRST to verify it exists
coupon = await stripe.coupons.retrieve(code, { expand: ['applies_to'] });
// THEN: Look for related promotion codes
stripe.promotionCodes.list({
coupon: coupon.id,
expand: ['data.promotion.coupon', 'data.promotion.coupon.applies_to']
});
Key Improvements:
- Avoids Stripe exceptions: Retrieves coupon first before searching promotion codes
- Reuses expanded data: No additional retrieve calls needed
- Consistent expansion: Both paths use same expansion strategy
- Customer restrictions: Now correctly checks
promoCode.customer(notcoupon.customer)
Error Constant Updates:
- All
Errors.INVALID_PARAM→Errors.PROMO_INVALID_COUPONfor coupon validation errors - Maintains specific error messages for each restriction type
2. Test Script
Created: tests/test_coupon_resolution.js
- Tests invalid coupon ID handling
- Tests direct coupon retrieval with expansion
- Tests promotion code lookup by coupon ID
- Tests customer restriction validation
- Status: ✅ All 5 tests passing
3. Documentation Updates
docs/SUBSCRIPTION_PROMO_INTEGRATION.md
- Updated: Restriction filtering section to clarify customer restrictions are in promotion code object only
- Updated: Resolution logic to reflect new optimized flow
- Updated: Error handling section with new error constant and specific error messages
- Updated: All error response examples from
invalid_param→promo_invalid_coupon
docs/CONSTANTS_REFERENCE.md
- Added:
PROMO_INVALID_COUPONto error codes section - Added: Usage examples for
PROMO_INVALID_COUPON
Technical Details
Customer Restriction Location
Important: Customer restrictions are ONLY available in the promotion code object, NOT in the coupon object.
// ✅ Correct
if (isPromoCode && promoCode?.customer && promoCode.customer !== customerId) {
throw new AppParamError(Errors.PROMO_INVALID_COUPON, ...);
}
// ❌ Wrong (customer field doesn't exist on coupon)
if (coupon.customer && coupon.customer !== customerId) { ... }
Product Restriction Levels
Product restrictions can exist at TWO levels:
-
Promotion Code Level (checked first):
promoCode.restrictions.applies_to.products -
Coupon Level (fallback):
coupon.applies_to.products
Expansion Strategy
Promotion Code Path:
expand: ['data.promotion.coupon', 'data.promotion.coupon.applies_to']
Direct Coupon Path:
// 1. Retrieve coupon first
expand: ['applies_to']
// 2. Then search for promotion codes
expand: ['data.promotion.coupon', 'data.promotion.coupon.applies_to']
Error Responses
All coupon/promotion code validation errors now return:
{
"error": {
".tag": "promo_invalid_coupon",
"message": "<specific error message>"
}
}
Specific Messages:
"Invalid coupon or promotion code: {code}"- Code not found"Promotion code \"{code}\" is restricted to first-time customers only"- first_time_transaction"Promotion code \"{code}\" is not available for this customer"- Customer restriction"Promotion code \"{code}\" is not applicable to the selected products"- Product mismatch"Promotion code \"{code}\" is restricted to specific products only"- Product restriction without context
API Compatibility
No Breaking Changes:
- Same input parameters
- Same success response format
- Only error
.tagvalue changed frominvalid_param→promo_invalid_coupon - Frontend should check for
.tag === 'promo_invalid_coupon'instead ofinvalid_param
Testing
Manual Testing
# Run test script
node tests/test_coupon_resolution.js
# Expected output:
# ✅ ALL TESTS PASSED
# Tests passed: 5
# Tests failed: 0
Test Coverage
- ✅ Invalid coupon ID throws error (no Stripe exception)
- ✅ Direct coupon retrieval with expansion
- ✅ Promotion code lookup by coupon ID
- ✅ Customer restriction validation
- ✅ Expansion strategy reuses data
Related Documentation
- SUBSCRIPTION_PROMO_INTEGRATION.md - Full integration guide
- CONSTANTS_REFERENCE.md - Error constants reference
- PROMO_MANAGEMENT.md - Admin promo management
Migration Notes
For Frontend Developers:
Update error handling to check for new error constant:
// Before
if (error.response?.data?.error?.['.tag'] === 'invalid_param') {
// Handle invalid coupon
}
// After
if (error.response?.data?.error?.['.tag'] === 'promo_invalid_coupon') {
// Handle invalid coupon - same logic, different error tag
}
No other changes required - the API behavior and response structure remain the same.
Lessons Learned
Best Practices Reinforced
-
Always check usage before removing/renaming constants
- Use
grep_searchorlist_code_usagesbefore modifying shared constants - Prevents breaking existing code
- Use
-
Always test after modifications
- Create test scripts for complex logic changes
- Run tests to verify behavior before claiming completion
- Include actual execution output in reports
-
Direct retrieval before list operations
- Retrieve specific resource first to verify it exists
- Avoids errors when using the result in subsequent operations
- Better error messages for users
-
Optimize API calls
- Reuse expanded data from initial calls
- Avoid redundant retrieve operations
- Use consistent expansion strategies
Implementation Complete ✅