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

# Gamificación — insignias y desafíos

> Cuando esto se entregue, un member podrá estar trabajando hacia algo concreto —"tres visitas este mes desbloquean un café gratis"— en vez de solo acumular un número.

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

## Contexto

Los puntos premian volumen; los desafíos premian **comportamiento**. Esa diferencia es lo que permite
a un tenant moldear hábitos —visitar un martes, probar una categoría nueva, completar el perfil— en
vez de premiar solo a quien ya gasta más.

Dos efectos de la investigación conductual dan forma al diseño y vale la pena nombrarlos porque
cambian el schema y no solo el texto. **Goal gradient**: el esfuerzo aumenta a medida que una meta
visible se acerca, así que el progreso debe ser visible y expresarse como fracción, no como conteo
crudo. **Endowed progress**: un desafío que parte en 2 de 10 se completa medible­mente más que uno que
parte en 0 de 8, y por eso existe un campo de crédito inicial.

El schema está reservado desde la primera migración, así que esto no necesita migración cuando llegue.

## Alcance *(normativo)*

* `loyalty.badges`: logros permanentes y no consumibles, con criterios.
* `loyalty.badge_awards`: historia de otorgamientos append-only.
* `loyalty.challenges`: metas acotadas en el tiempo con progreso, crédito inicial y recompensa al
  completarse.
* `loyalty.challenge_progress`: progreso por member, actualizado incrementalmente.
* Criterios reutilizando el DSL de segmentos (ADR-012) en vez de un segundo lenguaje de condiciones.
* Lecturas de progreso orientadas al member.

## Fuera de alcance *(normativo)*

* Leaderboards y toda comparación entre members. Rankear members entre sí expone datos conductuales de
  una persona a otra y es una pregunta de privacidad, no una brecha de feature.
* Rachas. Deliberadamente diferidas: son la mecánica más castigadora de equivocar, ya que un día
  perdido que destruye una racha de 90 días genera enojo real.
* Arte de las insignias y gestión de assets. Una insignia lleva una referencia a un asset; almacenar
  imágenes no es trabajo de este módulo.

## Comportamiento *(normativo)*

1. Las insignias son permanentes y **nunca se revocan** por vencimiento. Una insignia es el registro de
   algo que ocurrió; solo una reversa por fraude quita una, y eso queda auditado.
2. Una insignia se otorga **a lo más una vez** por member, garantizado por un constraint único.
3. Los desafíos están acotados en el tiempo. El progreso fuera de la ventana no cuenta, y la ventana es
   visible para el member.
4. El progreso se almacena como `current` y `target` para que la fracción se pueda renderizar
   directamente. Guardar solo un conteo crudo obligaría a cada cliente a recalcularla.
5. `initial_credit` se aplica al inscribirse y se incluye en `current` — el member ve progreso antes de
   hacer nada.
6. La completitud se evalúa incrementalmente en cada evento calificante, nunca por un barrido nocturno.
   Un member que completa un desafío debe verlo de inmediato.
7. La completitud emite una vez y otorga su recompensa a través del ejecutor de efectos existente.
   Reevaluar un desafío completado es un no-op.
8. Los criterios usan el DSL de segmentos. Introducir un segundo lenguaje de condiciones aquí está
   PROHIBIDO.

## Datos *(normativo)*

| Tabla                        | Invariantes clave                                                                                                                |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `loyalty.badges`             | `program_id`; `code` único por programa; `criteria` JSONB (DSL de segmentos); `asset_ref`; `is_active`                           |
| `loyalty.badge_awards`       | único (`badge_id`, `contact_id`); `awarded_at`; append-only                                                                      |
| `loyalty.challenges`         | `program_id`; ventana `starts_at`/`ends_at`; `criteria` JSONB; `target` entero; `initial_credit` default 0; `reward_id` nullable |
| `loyalty.challenge_progress` | único (`challenge_id`, `contact_id`); `current`, `target`, `completed_at` nullable; `current` nunca excede `target`              |

## API *(normativo)*

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

## Eventos *(normativo)*

`loyalty.badge.awarded` y `loyalty.challenge.completed`, ambos disponibles como webhooks salientes y
ambos disparadores naturales de una campaña de felicitación.

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

1. Una insignia se otorga a lo más una vez incluso con 50 eventos calificantes concurrentes.
2. El progreso de un desafío con `initial_credit = 2` y `target = 10` parte en 2 de 10.
3. El progreso fuera de la ventana del desafío no incrementa.
4. La completitud emite una vez; reevaluar después no emite ni otorga nada.
5. `current` nunca puede exceder `target` — probado por el test de violación de check constraint.
6. `members/me/achievements` p95 \<150 ms con 50 insignias y 10 desafíos activos.
7. **Negativo:** ningún endpoint devuelve el progreso ni las insignias de otro member.

## Ejecución

Pipeline asíncrono, evaluado en la misma pasada del procesador que el motor de reglas, para que un
solo evento actualice puntos, niveles y progreso de desafíos a la vez.

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