> ## 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.

# Niveles y ventanas de calificación

> Cuando esto se entregue, un member podrá ser Gold, saber exactamente qué lo mantiene Gold, y acumular mejor por serlo.

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

## Contexto

Los niveles son el mecanismo de retención de un programa: le dan al member algo que perder. Eso los
convierte también en la feature más cargada emocionalmente del módulo — un descenso que el member no
vio venir es un ticket de soporte y muchas veces un cliente que se va.

Dos decisiones cargan casi todo el peso. **La ventana de calificación** —doce meses móviles versus
año calendario— cambia la sensación completa del programa: móvil es continua y justa, calendario crea
un empujón de fin de año y un precipicio en enero. Y **la política de descenso**, que necesita un
período de gracia no por corrección sino porque bajar a alguien en el instante en que cae bajo un
umbral es una falla de producto aunque sea aritméticamente correcto.

Para esto existen los puntos de estatus (ADR-011). Calificar con puntos canjeables significaría que un
member pierde estatus por gastar, lo que castiga exactamente el comportamiento que el programa
busca.

## Alcance *(normativo)*

* `loyalty.tier_definitions`: niveles ordenados con umbrales contra una métrica de calificación.
* `loyalty.tier_qualification_metrics`: paramétrica, semillas `is_system` `points_earned`, `spend`,
  `event_count`.
* Modos de ventana `rolling_days` y `calendar_year`.
* `loyalty.tier_memberships` con historia completa: entrada, salida, razón.
* Política de descenso explícita con período de gracia configurable.
* `loyalty.tier_overrides`: multiplicadores de acumulación, precios de recompensas y extensión de
  expiración por nivel.
* Recomputación guiada por eventos del libro, con una red de seguridad programada.

## Fuera de alcance *(normativo)*

* Notificaciones de nivel. Esta feature emite los eventos; las campañas son `messaging`.
* Asignación manual de nivel por el personal — una feature de consola posterior, con su propia
  semántica de auditoría.
* Visibilidad de recompensas gateada por nivel más allá del override de precio, que es asunto de
  FS-LOY-0005.

## Comportamiento *(normativo)*

1. Los niveles se ordenan por `level`, único por programa, con umbrales estrictamente crecientes. Dos
   niveles en el mismo `level` están PROHIBIDOS.
2. La calificación usa la métrica configurada sobre la ventana configurada. Los puntos canjeables
   **nunca** califican: gastar no debe costar estatus.
3. Los ascensos son inmediatos al cruzar un umbral. Un member nunca debería esperar por una buena
   noticia.
4. Los descensos **nunca son inmediatos**. El member entra en un período de gracia; si vuelve a
   calificar antes de que termine, no se registra descenso alguno. Solo al vencer se aplica y se
   emite.
5. La historia de membresía es append-only. El nivel de un member en cualquier fecha pasada debe ser
   reconstruible — eso es lo que responde "por qué me cobraron ese precio".
6. `tier_overrides` aplican en el momento de la transacción, no retroactivamente. Llegar a Gold no
   revalúa el canje de ayer.
7. La recomputación es guiada por eventos en las escrituras del libro y reverificada por un job
   programado según el tier de frescura del tenant. Una divergencia se alerta, nunca se corrige en
   silencio.
   7b. **Cuando la moneda de estatus está configurada para expirar** (OQ-LOY-02, FS-LOY-0002), la
   métrica de calificación baja sola y un member puede descender sin haber dejado de comprar. En
   esa configuración el período de gracia y el evento `tier.grace_started` son **obligatorios**, y
   `members/me/tier` DEBE mostrar la caída proyectada y su fecha antes de que ocurra. Una
   flexibilidad que sorprende a un member es un ticket de soporte, no una feature.
8. `tier_qualification_metrics` **no** es extensible por tenant: la métrica gobierna lógica del
   sistema.

## Datos *(normativo)*

| Tabla                                | Invariantes clave                                                                                                                                |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `loyalty.tier_definitions`           | `program_id`; `level` único por programa; `threshold`; `metric_code` FK; `window_mode` y `window_days`; `grace_period_days`; `is_active`         |
| `loyalty.tier_memberships`           | append-only; `contact_id`, `tier_id`, `entered_at`, `exited_at` nullable, `reason_code`; a lo más una membresía abierta por (programa, contacto) |
| `loyalty.tier_overrides`             | FK nivel; `earning_multiplier`, `reward_cost_multiplier`, `expiration_extension_days`, todos nullable = sin override                             |
| `loyalty.tier_qualification_metrics` | paramétrica; semillas `is_system` según alcance; no extensible por tenant                                                                        |

## API *(normativo)*

| Endpoint                           | Clase      | Permiso                                | Presupuesto  |
| ---------------------------------- | ---------- | -------------------------------------- | ------------ |
| `GET/POST/PATCH /v1/loyalty/tiers` | Management | `loyalty.tiers.{read\|create\|update}` | p95 \<1 s    |
| `GET /v1/loyalty/members/me/tier`  | Runtime    | token de member                        | p95 \<150 ms |

`members/me/tier` devuelve el nivel actual, el progreso hacia el siguiente y —cuando está en gracia—
la fecha en que se aplicaría el descenso. Ocultar esa fecha sería la decisión más hostil con el
usuario disponible aquí.

## Eventos *(normativo)*

`loyalty.tier.upgraded` y `loyalty.tier.downgraded`, ambos disponibles como webhooks salientes.
Entrar en gracia no emite evento en F1; si debería hacerlo es una pregunta abierta más abajo.

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

1. Cruzar un umbral asciende dentro de 5 s p95 desde la escritura calificante del libro y emite una
   vez.
2. Caer bajo un umbral inicia la gracia y **no** desciende. Volver a calificar dentro de la gracia no
   registra descenso alguno.
3. El vencimiento de la gracia desciende y emite exactamente una vez.
4. Corrección de ventana móvil: un member cuya actividad calificante tiene 366 días deja de contarla,
   verificado contra una línea de tiempo sembrada.
5. La historia de membresía reconstruye el nivel que tenía en cualquier fecha pasada.
6. Un multiplicador de override aplica a las acumulaciones posteriores al ascenso y no a las
   anteriores.
7. **Negativo:** canjear puntos nunca baja la métrica de calificación de un member.

## Ejecución

Pipeline asíncrono, guiado por eventos del libro; la recomputación de red de seguridad es un job
programado con fan-out por tenant.

## 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.*
