> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Reglas de acumulación de beneficios

> Cuando esto se entregue, un tenant podrá declarar qué beneficios se pueden combinar, y el motor lo hará cumplir en vez de que el cajero improvise.

> Traducción. Autoritativo: [`fs-loy-0008-stacking-rules.md`](/modules/loyalty/features/fs-loy-0008-stacking-rules).

## Contexto

En cuanto un programa tiene más de un instrumento —puntos, cupones, descuento por nivel— aparece la
pregunta "¿estos se pueden usar juntos?", y es una pregunta de margen. Sin una política explícita la
respuesta se decide ad hoc en el mostrador, que es como los programas pierden dinero en silencio.

Esto deliberadamente **no** es parte de cupones ni de canjes. Es una capa de política sobre ambos, y
forzarla dentro de cualquiera de los dos produce un spec que no se puede aprobar porque la mitad de
su materia vive en otro lado. Mantenerlo separado es también lo que permite que F1a salga sin esto:
hasta que un tenant tenga dos instrumentos combinables, la política es trivialmente "sí".

## Alcance *(normativo)*

* `loyalty.discount_categories`: paramétrica, extensible por tenant, con orden de precedencia.
* `loyalty.stacking_rules`: política ALL o PARTIAL por programa, más conjuntos no combinables.
* Evaluación dentro de la validación del canje: dado un conjunto de beneficios pretendidos, devolver
  el subconjunto permitido y la razón de cada exclusión.
* Precedencia determinista cuando dos beneficios entran en conflicto.

## Fuera de alcance *(normativo)*

* Calcular el precio resultante. Eso es `apply_discount`, F2.
* Acumulación entre tenants o entre programas. Un beneficio nunca se acumula entre programas.
* Optimización automática a favor del member. El motor aplica la política; no busca la combinación
  que más le convendría al cliente. Esa es una pregunta deliberada de F2 en adelante.

## Comportamiento *(normativo)*

1. La política por defecto de un programa nuevo es **PARTIAL sin conjuntos no combinables**: todo se
   acumula hasta que el tenant diga lo contrario. Se elige el default permisivo porque el
   restrictivo rompe en silencio los programas existentes al actualizar.
2. La evaluación es una **función pura** sobre los beneficios pretendidos y la política del programa.
   Sin I/O, totalmente testeable unitariamente, determinista para una entrada dada.
3. La precedencia es explícita y total: las categorías llevan `sort_order` y los empates se rompen por
   código de categoría. No existe escenario donde el resultado dependa del orden del request.
4. Toda exclusión devuelve una **razón tipada**. "No permitido" sin razón es inusable en un mostrador
   e inusable en soporte.
5. La evaluación corre dentro de la validación del canje, antes de cualquier escritura. PROHIBIDO:
   aplicar beneficios y después revertir los que no debieron acumularse.
6. `discount_categories` es extensible por tenant — es su vocabulario comercial. La política de
   acumulación en sí no lo es: ALL y PARTIAL son los únicos modos.

## Datos *(normativo)*

| Tabla                         | Invariantes clave                                                                                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `loyalty.discount_categories` | paramétrica, **extensible por tenant**; `code`, `label`, `sort_order` obligatorio y único por programa                                                  |
| `loyalty.stacking_rules`      | `program_id`; `mode` ALL o PARTIAL; `non_combinable_sets` JSONB, cada uno un conjunto de códigos de categoría; exactamente una fila activa por programa |

## API *(normativo)*

| Endpoint                                   | Clase      | Permiso                                 | Presupuesto |
| ------------------------------------------ | ---------- | --------------------------------------- | ----------- |
| `GET/PUT /v1/loyalty/stacking-rules`       | Management | `loyalty.stacking_rules.{read\|update}` | p95 \<1 s   |
| `POST /v1/loyalty/stacking-rules/simulate` | Management | `loyalty.stacking_rules.simulate`       | p95 \<1 s   |

## Eventos *(normativo)*

Ninguno. Los cambios de política son administrativos y quedan cubiertos por el registro de auditoría.

## Criterios de aceptación *(normativo)*

1. Un programa nuevo acumula todo por defecto, verificado sobre un tenant recién aprovisionado.
2. El modo ALL permite toda combinación; PARTIAL con un conjunto no combinable
   `{tier_discount, coupon}` rechaza exactamente ese par y permite todos los demás.
3. La evaluación es independiente del orden: el mismo conjunto de beneficios en seis órdenes
   distintos arroja el mismo subconjunto permitido.
4. Toda exclusión lleva una razón tipada que nombra la regla que la excluyó.
5. 100% de cobertura de ramas sobre la función pura de evaluación.
6. **Negativo:** ningún canje aplica un beneficio que la evaluación excluyó — probado por un test que
   lo intenta directamente contra el servicio.

## Ejecución

Un solo slice, comando síncrono. El evaluador vive en `packages/core` junto a `resolveChannels` —
ambas son funciones puras de política con la misma disciplina de testing.

## Preguntas abiertas

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

## Changelog

| Versión | Fecha      | Cambio                                                                             | Por qué                                    | Autor                  |
| ------- | ---------- | ---------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- |
| 0.2.0   | 2026-08-17 | Preguntas abiertas resueltas (OQ-LOY-\*) e incorporadas a las secciones normativas | El owner respondió el registro consolidado | daniel + claude-opus-5 |
| 0.1.0   | 2026-08-17 | Borrador inicial                                                                   | —                                          | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
