241 lines
6.7 KiB
Markdown
241 lines
6.7 KiB
Markdown
# 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 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 `.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** ✅
|