Traducción. Autoritativo: fs-loy-0006-redemptions.md.
Contexto
Este es el endpoint que llama el terminal de un cajero con un cliente parado frente al mostrador. Es el camino más hostil operacionalmente de todo el módulo: redes poco fiables, requests reintentados, ventas anuladas y una persona esperando. Tres propiedades no son negociables. Idempotencia, porque un POS va a reintentar y un canje duplicado es un incidente de atención al cliente. Atomicidad entre varias recompensas, porque un carro puede canjear más de una cosa y medio canje es peor que ninguno. Y rollback, porque las ventas se anulan y los puntos tienen que volver con una decisión sobre los lotes que consumieron. DEC-H3 confirmó el canje mixto puntos + dinero como requisito real — es una feature que gana contratos. F1 no lo construye, pero deja la costura: unmoney_component nullable y un evento de
dominio con la forma necesaria para que agregar el pago después sea una adición y no una cirugía.
Alcance (normativo)
loyalty.redemptions, filas padre e hijo desde la primera migración.- Manejo de
Idempotency-Keycon resultado almacenado por 24 horas. - Validación contra saldo, disponibilidad de la recompensa, stock y límites por contacto.
- Transacciones
redeemdel libro consumiendo lotes FIFO, a través del servicio de FS-LOY-0002. - Rollback con dos modos explícitos:
revert(devolver los puntos) ykeep(no devolverlos). money_component, nullable, sin uso en F1.- La lectura
members/meorientada al member: saldo, nivel, cupones activos.
Fuera de alcance (normativo)
- La política de acumulación entre varios descuentos — FS-LOY-0008. Aquí un canje aplica lo que se le indica; decidir qué se puede combinar es una capa de política por encima.
- El canje de cupones — FS-LOY-0007. Cupones y puntos son instrumentos distintos.
- Cualquier ejecución de pago. La costura está reservada, no cableada.
Comportamiento (normativo)
Idempotency-Keyes obligatorio. La misma clave reproduce el resultado almacenado durante 24 horas, byte por byte, sin re-ejecutar nada. Un payload distinto bajo la misma clave es un error tipado de conflicto, nunca una sobreescritura silenciosa.- Un canje es una transacción: validación, filas
redeemdel libro, consumo de lotes, decremento de stock, actualización de la proyección, evento del outbox y fila de auditoría confirman juntos. - Los requests con varias recompensas son padre/hijo. El padre es todo o nada: si cualquier hijo falla, no se aplica nada.
- La validación ocurre antes de cualquier escritura. Saldo insuficiente, recompensa inactiva o sin stock, o límite por contacto agotado fallan limpiamente con un error tipado y cero efectos secundarios.
- El rollback es a nivel de padre con modo explícito.
revertescribe transacciones compensatorias que restauran los puntos; los lotes consumidos se restauran a sus lotes originales siempre que no hayan expirado, y a un lote nuevo que hereda el vencimiento más próximo cuando sí.keepdeja los puntos gastados y registra la decisión. No hay valor por defecto: el llamador declara el modo. - Un canje nunca se borra. El rollback es una transición de estado más transacciones compensatorias.
money_componentes nullable y DEBE ser null en F1. Un valor no nulo se rechaza hasta que la costura de pagos esté implementada.- Presupuesto: p95 <300 ms para la decisión síncrona (DEC-D7).
Datos (normativo)
API (normativo)
Eventos (normativo)
loyalty.points.redeemed sobre un padre exitoso, con el id del canje, las recompensas, el costo
total y el correlation_id. Un rollback lo emite de nuevo con una marca reverted en vez de
inventar un segundo nombre de evento — un consumidor que entiende un canje entiende su reversa.
Criterios de aceptación (normativo)
- La misma
Idempotency-Keyenviada 100 veces en paralelo produce exactamente un canje y un solo conjunto de filas del libro. - Un payload distinto bajo una clave ya usada devuelve un conflicto tipado, y el canje original queda sin cambios.
- Un padre de tres recompensas donde la tercera excede el stock no aplica nada: sin filas del libro, sin decremento de stock, sin padre parcial.
revertrestaura el total exacto de puntos; la suma sobre el libro del member vuelve a su valor previo al canje. Los puntos restaurados que caen en un lote nuevo heredan el vencimiento más próximo de los lotes consumidos.keepdeja el saldo sin cambios y registra el modo en la fila del canje.- p95 <300 ms bajo el perfil k6 con un dataset de 10 000 contactos.
- Negativo: un request con
money_componentno nulo se rechaza en F1 con un error tipado, no se ignora en silencio.
Ejecución
Arquetipo comando síncrono. Se entrega como slice propio después de TS-004: el endpoint enbackend/api, el servicio de dominio junto al servicio del libro, sin worker — este camino es
síncrono por diseño porque el cajero está esperando.
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.