# Coupon & Promotion Code Validation Updates **Date**: February 2, 2026 **Status**: ✅ Complete --- ## Summary Updated coupon and promotion code validation logic in `resolveCouponCode()` to: 1. Fix direct coupon ID resolution flow (retrieve coupon first to avoid Stripe exceptions) 2. Clarify that customer restrictions are only available in promotion code objects 3. Add new error constant `PROMO_INVALID_COUPON` for all coupon/promo validation errors 4. 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**: ```javascript // 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**: 1. **Avoids Stripe exceptions**: Retrieves coupon first before searching promotion codes 2. **Reuses expanded data**: No additional retrieve calls needed 3. **Consistent expansion**: Both paths use same expansion strategy 4. **Customer restrictions**: Now correctly checks `promoCode.customer` (not `coupon.customer`) **Error Constant Updates**: - All `Errors.INVALID_PARAM` → `Errors.PROMO_INVALID_COUPON` for 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_COUPON` to 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. ```javascript // ✅ 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: 1. **Promotion Code Level** (checked first): ```javascript promoCode.restrictions.applies_to.products ``` 2. **Coupon Level** (fallback): ```javascript coupon.applies_to.products ``` ### Expansion Strategy **Promotion Code Path**: ```javascript expand: ['data.promotion.coupon', 'data.promotion.coupon.applies_to'] ``` **Direct Coupon Path**: ```javascript // 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: ```json { "error": { ".tag": "promo_invalid_coupon", "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 `.tag` value changed from `invalid_param` → `promo_invalid_coupon` - Frontend should check for `.tag === 'promo_invalid_coupon'` instead of `invalid_param` --- ## Testing ### Manual Testing ```bash # Run test script node tests/test_coupon_resolution.js # Expected output: # ✅ ALL TESTS PASSED # Tests passed: 5 # Tests failed: 0 ``` ### Test Coverage 1. ✅ Invalid coupon ID throws error (no Stripe exception) 2. ✅ Direct coupon retrieval with expansion 3. ✅ Promotion code lookup by coupon ID 4. ✅ Customer restriction validation 5. ✅ Expansion strategy reuses data --- ## Related Documentation - [SUBSCRIPTION_PROMO_INTEGRATION.md](./SUBSCRIPTION_PROMO_INTEGRATION.md) - Full integration guide - [CONSTANTS_REFERENCE.md](./CONSTANTS_REFERENCE.md) - Error constants reference - [PROMO_MANAGEMENT.md](./PROMO_MANAGEMENT.md) - Admin promo management --- ## Migration Notes **For Frontend Developers**: Update error handling to check for new error constant: ```javascript // 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 1. **Always check usage before removing/renaming constants** - Use `grep_search` or `list_code_usages` before modifying shared constants - Prevents breaking existing code 2. **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 3. **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 4. **Optimize API calls** - Reuse expanded data from initial calls - Avoid redundant retrieve operations - Use consistent expansion strategies --- **Implementation Complete** ✅