Skip to main content
Traducción. Autoritativo: fs-loy-0007-coupons.md.

Contexto

Los cupones son el segundo instrumento del módulo, y no son puntos. Un punto es un saldo que un member acumula; un cupón es un token al portador con su propio ciclo de vida, su propia superficie de fraude y su propio camino de canje. Voucherify construyó su negocio sobre esa diferencia, y las lecciones operativas están bien establecidas: los códigos únicos deben generarse en volumen sin colisiones, y la validación debe ser una sola llamada rápida porque ocurre en un mostrador. La superficie de fraude es lo que hace de esto una feature propia y no un campo de una recompensa. Un código que se puede adivinar, compartir o canjear dos veces es pérdida directa de ingresos, y cada uno de esos casos es un control distinto.

Alcance (normativo)

  • loyalty.coupon_campaigns: genéricas (un código compartido) o únicas (un código por member).
  • Generación masiva de códigos únicos con un alfabeto libre de colisiones y un presupuesto de entropía documentado.
  • loyalty.coupon_codes con estados issued → redeemed | expired | void.
  • Endpoint de validación: ¿este código es válido, para este member, ahora?
  • Canje, quemando el código atómicamente.
  • issue_coupon como efecto de regla, cableado al ejecutor de FS-LOY-0004.

Fuera de alcance (normativo)

  • Si un cupón se puede combinar con otro descuento — FS-LOY-0008.
  • Descontar el precio de un carro. Un cupón aquí registra que se canjeó; calcular un precio nuevo es apply_discount, F2 (DEC-H4).
  • La distribución. Enviar el código al member es messaging, alcanzado por el evento de emisión.

Comportamiento (normativo)

  1. Un código es de 12 caracteres sobre Crockford Base32 (sin I, L, O, U) por defecto, único por tenant, insensible a mayúsculas al validar. Una campaña puede bajar a 10 cuando el código se dicta por teléfono. Doce caracteres dan ~1,15×10¹⁸ combinaciones, así que la generación masiva nunca pelea consigo misma ni con cientos de millones de códigos entre todos los tenants (OQ-LOY-11).
  2. La entropía es un presupuesto declarado, no un accidente: suficiente para que adivinar sea inviable frente a los rate limits de la campaña. Códigos secuenciales o predecibles están PROHIBIDOS.
  3. La generación masiva es idempotente por request y nunca produce una colisión. Reintentar un request de generación no crea un segundo lote.
  4. La validación es de solo lectura y nunca muta. Responde válido o inválido con una razón tipada —expirado, ya canjeado, member incorrecto, campaña inactiva— porque un cajero necesita saber cuál.
  5. El canje quema el código en una transacción: transición de estado, evento del outbox, fila de auditoría. Un segundo canje del mismo código falla con un error tipado, siempre.
  6. Un código nunca se borra. Cancelarlo significa el estado void.
  7. Una campaña de código único vincula cada código a un contacto. Una campaña genérica no lo hace, y su límite de uso por contacto se aplica mediante el historial de canjes.

Datos (normativo)

API (normativo)

Eventos (normativo)

loyalty.coupon.issued, loyalty.coupon.redeemed, loyalty.coupon.expired — todos disponibles como webhooks salientes. issued es lo que escucha messaging para entregar el código.

Criterios de aceptación (normativo)

  1. Generar 100 000 códigos únicos produce cero colisiones y termina dentro del presupuesto documentado.
  2. Reintentar un request de generación con la misma clave de idempotencia no crea un segundo lote.
  3. La validación devuelve una razón tipada distinta para cada caso inválido y no escribe nada.
  4. Canjear el mismo código dos veces en paralelo (50 requests) tiene éxito exactamente una vez.
  5. Un código vinculado al member A validado para el member B se rechaza con WRONG_MEMBER.
  6. El vencimiento mueve un código a expired y emite el evento exactamente una vez.
  7. Negativo: ningún endpoint borra un código de cupón.

Ejecución

Comando síncrono para validación y canje; la generación masiva es un job encolado para lotes sobre un umbral documentado, idempotente vía core.processed_jobs.

Preguntas abiertas

Ninguna pendiente. Toda pregunta que llevaba este spec quedó respondida en el registro consolidado (../../../../design/open-questions-v1.md, v1.1) y se incorporó a las secciones normativas de arriba.

Changelog

Registro de entrega

Aún no implementado.