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

# Listas y vistas guardadas

> Cuando esto se entregue, "las doce personas que fueron al lanzamiento" es algo que un tenant puede guardar, y ninguna regla de segmento lo habría podido expresar.

> Traducción. Autoritativo: [`fs-crm-0003-lists-and-views.md`](/modules/crm/features/fs-crm-0003-lists-and-views).

## Contexto

`core` ya tiene segmentos dinámicos y cubren la mayoría de las necesidades. Lo que no pueden expresar
es lo arbitrario: un grupo piloto elegido a mano, los asistentes a un evento, las ocho cuentas que un
ejecutivo atiende personalmente. Eso no tiene regla; tiene el criterio de una persona.

Dos cosas distintas comparten una feature porque los tenants las piensan como una:

* Una **lista estática** es membresía explícita. Alguien está porque alguien lo puso.
* Una **vista guardada** es un filtro almacenado sobre contactos. Es un atajo, y su membresía cambia
  cuando cambian los datos.

La distinción importa al momento de enviar. Una lista es estable — los miembros son los que eran la
semana pasada. Una vista se evalúa al usarla y puede haber cambiado. Confundirlas es cómo una campaña
llega a una audiencia distinta de la que la persona esperaba.

## Alcance *(normativo)*

* `crm.lists`: estática o vista guardada, por tenant.
* `crm.list_memberships` para listas estáticas.
* Definición de filtro para vistas guardadas, reusando la gramática del DSL de segmentos.
* Agregar y quitar miembros individual y masivamente.
* Usar una lista como audiencia de campaña.
* Exportación CSV de una lista, gateada por permiso y auditada.

## Fuera de alcance *(normativo)*

* Segmentos dinámicos, que son de `core`.
* Compartir listas entre tenants. Nunca.
* Automatización sobre cambios de membresía — eso lo hace un segmento, y una lista no debería volverse
  un segundo sistema de disparadores.

## Comportamiento *(normativo)*

1. Una lista es `static` o `saved_view`, declarada al crearse e **inmutable después**. Convertir una en
   otra cambia en silencio qué significa "los miembros".
2. La membresía estática es explícita, con **quién agregó el contacto y cuándo**. Esa atribución es lo
   que hace defendible una lista cuando alguien pregunta por qué una persona recibió una campaña.
3. Una vista guardada almacena un filtro con la **gramática del DSL de segmentos** (ADR-012) en vez de
   un segundo lenguaje de consulta. Su membresía se evalúa en lectura y nunca se materializa.
4. Una vista guardada usada como audiencia de campaña se evalúa **al despachar** y la membresía
   resuelta se congela en la corrida de la campaña, para que la audiencia sea reproducible después.
5. Quitar un contacto de una lista estática es lógico: `removed_at`, conservado. "Quién estaba en esta
   lista en marzo" tiene que ser respondible.
6. La supresión quita al contacto de toda lista y borra sus filas de membresía.
7. La exportación tiene su propio permiso y queda **auditada como acceso masivo**, como cualquier
   lectura masiva de datos personales.
8. El alta masiva está acotada y es idempotente: agregar el mismo contacto dos veces deja una membresía
   activa.

## Datos *(normativo)*

| Tabla                  | Invariantes clave                                                                                                                  |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `crm.lists`            | `tenant_id`; `name` único por tenant; `kind` static\|saved\_view, inmutable; `filter` JSONB para vistas; `created_by`; `is_active` |
| `crm.list_memberships` | (`list_id`, `contact_id`) con una fila activa; `added_by`, `added_at`, `removed_at` nullable; conservada tras quitarse             |

## API *(normativo)*

| Endpoint                                        | Clase      | Permiso                            | Presupuesto       |
| ----------------------------------------------- | ---------- | ---------------------------------- | ----------------- |
| `GET/POST/PATCH /v1/crm/lists`                  | Management | `crm.lists.{read\|create\|update}` | p95 \<1 s         |
| `GET /v1/crm/lists/{id}/members`                | Management | `crm.lists.read`                   | p95 \<1 s, cursor |
| `POST /v1/crm/lists/{id}/members`               | Management | `crm.lists.update`                 | p95 \<1 s, masivo |
| `DELETE /v1/crm/lists/{id}/members/{contactId}` | Management | `crm.lists.update`                 | p95 \<1 s         |
| `GET /v1/crm/lists/{id}/export`                 | Management | `crm.lists.export`, auditado       | asíncrono         |

## Eventos *(normativo)*

`crm.list.membership_changed`, para que una campaña o integración pueda reaccionar. No se ofrece como
webhook saliente por defecto.

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

1. El tipo de una lista no se puede cambiar tras crearse.
2. La membresía estática registra quién agregó el contacto y cuándo.
3. La membresía de una vista guardada se evalúa en lectura y nunca se materializa.
4. Una vista usada como audiencia de campaña congela su membresía resuelta en la corrida.
5. Quitar es lógico: "quién estaba en esta lista en una fecha pasada" es respondible.
6. La supresión quita al contacto de toda lista.
7. La exportación exige `crm.lists.export` y escribe una fila de auditoría de acceso masivo.
8. Agregar masivamente el mismo contacto dos veces deja una membresía activa.
9. **Negativo:** ninguna lista referencia un contacto de otro tenant.

## Ejecución

Un solo slice, comando síncrono. La evaluación de vistas guardadas reusa el evaluador del DSL de
`packages/core` — una gramática, un evaluador, un conjunto de tests.

## Preguntas abiertas

| # | Pregunta                                                                   | Decide | Para             |
| - | -------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | ¿Tamaño máximo de una lista estática antes de que debiera ser un segmento? | Daniel | antes de aprobar |
| 2 | ¿Una vista guardada puede referenciar un segmento dinámico como condición? | 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.*
