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

# Timeline 360

> Cuando esto se entregue, un agente de soporte verá "compró el martes → una regla acreditó 450 puntos → le escribimos → lo abrió → canjeó el jueves" como una sola cadena, en una pantalla.

> Traducción. Autoritativo: [`fs-crm-0004-timeline.md`](/modules/crm/features/fs-crm-0004-timeline).

## Contexto

Esta es la feature que justifica el módulo, y lo único de la suite que los competidores no pueden
copiar estructuralmente. Una herramienta de loyalty ve puntos. Una plataforma de correo ve envíos. Un
CRM ve notas. Solo un sistema donde los tres comparten un `correlation_id` (DEC-E7) puede mostrar la
cadena causal en vez de tres listas ordenadas por fecha.

Es **una proyección, no una tabla**. Materializarla sería una quinta copia de datos que ya existen,
mantenida en sincronía por un job que va a driftear. Se mezcla en vivo desde las fuentes al leer.

La trampa de permisos es real: una vista unida es justamente donde se olvida un permiso. Un agente sin
`core.contacts.read_national_id` debe ver el documento enmascarado también acá, y uno sin
`loyalty.ledger.read` no debe enterarse de saldos por el timeline.

## Alcance *(normativo)*

* Una lectura ordenada por mezcla sobre `core.tracked_events`, `loyalty.ledger_transactions`,
  `messaging.sends` con estado, `crm.notes` y `crm.activities`.
* Paginación por cursor sobre `occurred_at`.
* Filtrado por fuente y por rango de fechas.
* Renderizado de cadena: las entradas que comparten `correlation_id` se agrupan como una secuencia
  causal.
* Filtrado por permisos por fuente.

## Fuera de alcance *(normativo)*

* Materializar el timeline. Se calcula en lectura, siempre.
* Reportería entre contactos. Esto es la historia de un contacto.
* Editar cualquier cosa desde el timeline. Es una superficie de lectura.
* Streaming en tiempo real — F1b si se pide.

## Comportamiento *(normativo)*

1. El timeline se **calcula en lectura** desde sus fuentes. No hay tabla, ni vista materializada, ni
   job de sincronización.
2. Cada fuente se consulta **con su predicado de clave de partición**. Por eso el rango de fechas es
   **obligatorio**, no con valor por defecto, y la API lo dice — un timeline sin rango es un scan
   completo de las tablas más grandes de la plataforma.
3. Las entradas se ordenan por mezcla sobre `occurred_at` descendente, con paginación por cursor. Las
   fuentes se consultan en paralelo y se mezclan, nunca se traen completas para ordenar después.
4. Cada fuente se **filtra por su propio permiso de forma independiente**. Quien no tenga el permiso de
   una fuente no ve sus entradas, y no se entera de que existen por un hueco o un conteo.
5. Los campos sensibles se enmascaran según los permisos de quien lee, **exactamente como en el módulo
   dueño** (DEC-A4). El timeline nunca se vuelve el atajo.
6. Las entradas que comparten `correlation_id` se **agrupan como una cadena** con su orden causal
   preservado. Ese agrupamiento es la feature; una lista plana por fecha es lo que ya ofrece cualquier
   otra herramienta.
7. Las lecturas entre módulos van por el **camino público de lectura de cada uno, nunca sus tablas**. El
   límite se mantiene incluso para leer.
8. Una fuente no disponible **degrada con gracia**: el timeline renderiza el resto y dice cuál falta.
   Una caída de mensajería no debería dejar en blanco la pantalla de un agente.

## Datos *(normativo)*

Ninguna tabla. Esta feature posee una proyección y ningún almacenamiento — que es justamente el punto.

## API *(normativo)*

| Endpoint                             | Clase      | Permiso             | Presupuesto                                 |
| ------------------------------------ | ---------- | ------------------- | ------------------------------------------- |
| `GET /v1/crm/contacts/{id}/timeline` | Management | `crm.timeline.read` | p95 \<1 s primera página, rango obligatorio |

El permiso único gatea el acceso al timeline; **cada fuente se filtra además por su propio permiso**.
Tener solo `crm.timeline.read` muestra notas y actividades y nada más.

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

1. Primera página p95 \<1 s para un contacto con 5 000 eventos entre todas las fuentes.
2. Un request sin rango de fechas se rechaza con error tipado.
3. Toda consulta de fuente lleva su predicado de partición — verificado capturando las consultas.
4. Quien no tenga el permiso de una fuente no ve sus entradas ni las puede inferir por conteos o huecos.
5. Un documento de identidad aparece enmascarado para quien no tenga `core.contacts.read_national_id`.
6. Las entradas que comparten `correlation_id` se renderizan como una cadena en orden causal — el caso
   compra → puntos → correo → apertura → canje, end to end.
7. Una fuente que devuelve error degrada con gracia y nombra la fuente faltante.
8. La paginación por cursor devuelve cada entrada exactamente una vez entre páginas, sin duplicados en
   los bordes.
9. **Negativo:** ninguna tabla de otro módulo se lee directamente, impuesto por dependency-cruiser.

## Eventos *(normativo)*

Ninguno. Una superficie de lectura no emite nada.

## Ejecución

Un solo slice, comando síncrono. Fuentes consultadas en paralelo con timeout por fuente; mezcla y
filtrado de permisos en `backend/api`.

## Preguntas abiertas

| # | Pregunta                                                                   | Decide | Para             |
| - | -------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | Rango por defecto cuando el llamador no lo entrega — ¿rechazar, o 90 días? | Daniel | antes de aprobar |
| 2 | Timeout por fuente antes de degradar — ¿300 ms?                            | Daniel | antes de aprobar |

## Changelog

| Versión | Fecha      | Cambio           | Por qué | Autor                  |
| ------- | ---------- | ---------------- | ------- | ---------------------- |
| 0.1.0   | 2026-08-17 | Borrador inicial | —       | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
