agmission/Development/server/docs/archived/COUPON_VALIDATION_UPDATES.md

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**