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

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:

  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:

// 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_PARAMErrors.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_parampromo_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.

// ✅ 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):

    promoCode.restrictions.applies_to.products
    
  2. 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 .tag value changed from invalid_parampromo_invalid_coupon
  • Frontend should check for .tag === 'promo_invalid_coupon' instead of invalid_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

  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


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

  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