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_codescon estadosissued → 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_couponcomo 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)
- 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).
- 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.
- 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.
- 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.
- 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.
- Un código nunca se borra. Cancelarlo significa el estado
void. - 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)
- Generar 100 000 códigos únicos produce cero colisiones y termina dentro del presupuesto documentado.
- Reintentar un request de generación con la misma clave de idempotencia no crea un segundo lote.
- La validación devuelve una razón tipada distinta para cada caso inválido y no escribe nada.
- Canjear el mismo código dos veces en paralelo (50 requests) tiene éxito exactamente una vez.
- Un código vinculado al member A validado para el member B se rechaza con
WRONG_MEMBER. - El vencimiento mueve un código a
expiredy emite el evento exactamente una vez. - 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íacore.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.