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

# Consentimiento como historia

> Cuando esto se entregue, podremos demostrar —con timestamp y evidencia— que una persona concreta aceptó ser contactada por un canal concreto para una finalidad concreta, y exactamente cuándo dejó de aceptarlo.

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

## Contexto

La Ley 21.719 entra en plena vigencia el 2026-12-01, y el punto que da forma a este diseño es que
fiscaliza **evidencia operativa, no políticas**. Una columna booleana que diga `email_opt_in = true`
no prueba nada: no puede decir cuándo, cómo, ni qué se le mostró a la persona.

Por eso el consentimiento es una entidad con historia. Cada otorgamiento y cada revocación es una
fila con prueba y timestamp, y el estado actual se deriva de la última fila. Sobreescribir es lo que
destruye la evidencia, así que sobreescribir está prohibido.

Esto además tiene peso comercial directo: el consentimiento define el **contacto accionable**, que es
la base facturable de toda la suite (ADR-014). Una revocación reduce la factura del ciclo siguiente
sin ventana de castigo — esa es una promesa de precio implementada aquí.

## Alcance *(normativo)*

* `core.consents`: historia append-only por (contacto, canal, finalidad).
* `core.consent_purposes`: paramétrica — `transactional`, `marketing`, `product`.
* Captura de prueba: origen, IP, user agent, versión del texto mostrado, timestamp.
* Resolución de estado actual: gana la última fila.
* El predicado de contacto accionable que usan medición y mensajería.
* Endpoints de otorgar y revocar, más una lectura de preferencias para el member.

## Fuera de alcance *(normativo)*

* Decidir si enviar. `messaging` lee el consentimiento; la decisión vive en `resolveChannels`.
* Las supresiones, que son otra cosa: el consentimiento es lo que la persona eligió, una supresión
  es lo que el proveedor o el sistema impusieron tras un rebote duro. Nunca deben confundirse.
* La UI del centro de preferencias — `frontend/portal`.

## Comportamiento *(normativo)*

1. El consentimiento es **append-only**. Una revocación es una fila nueva, nunca un update.
   PROHIBIDO: un booleano mutable en cualquier parte que represente consentimiento.
2. Toda fila lleva **prueba**: origen, timestamp de captura e identificador de la versión del texto
   que se mostró. Consentimiento sin prueba no es consentimiento.
3. El estado actual por (contacto, canal, finalidad) es la fila con mayor `captured_at`. Los empates
   son imposibles: la columna es timestamptz con precisión de microsegundo más una secuencia
   monótona.
4. **La finalidad transaccional no es opt-out** (DEC-E3). Quien compró algo recibe su comprobante.
   Solo una supresión por rebote duro lo detiene.
5. La ausencia de consentimiento **no** es consentimiento. El estado por defecto para marketing es
   denegado, en todas partes, sin override por tenant.
6. Una revocación surte efecto **de inmediato** — antes de la siguiente decisión de envío, no en el
   recálculo nocturno.
7. Un **contacto accionable** es el que tiene un otorgamiento activo en ≥1 canal para una finalidad
   no transaccional, y no está suprimido. Ese predicado se define aquí una vez y lo consume la
   medición.
8. El consentimiento sobrevive a una fusión: el superviviente hereda la unión, y donde ambos lados
   difieren **gana lo más restrictivo**. Heredar un otorgamiento que la persona nunca dio para ese
   registro es exactamente la falla que esta regla evita.
9. La supresión borra las filas de consentimiento junto con el perfil, pero el registro agregado de
   prueba de tratamiento en la auditoría sobrevive (DEC-J3).

## Datos *(normativo)*

| Tabla                   | Invariantes clave                                                                                                                                                                                                                                              |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.consents`         | append-only; FK `contact_id`, `channel_code`, `purpose_code`; `status` granted\|revoked; `captured_at` timestamptz; `source`, `ip`, `user_agent`, `text_version`; índice sobre (`tenant_id`, `contact_id`, `channel_code`, `purpose_code`, `captured_at DESC`) |
| `core.consent_purposes` | paramétrica; semillas `is_system` `transactional`, `marketing`, `product`; no extensible por tenant                                                                                                                                                            |

## API *(normativo)*

| Endpoint                               | Clase      | Permiso               | Presupuesto  |
| -------------------------------------- | ---------- | --------------------- | ------------ |
| `POST /v1/core/contacts/{id}/consents` | Management | `core.consents.write` | p95 \<1 s    |
| `GET /v1/core/contacts/{id}/consents`  | Management | `core.consents.read`  | p95 \<1 s    |
| `GET /v1/core/members/me/preferences`  | Runtime    | token de member       | p95 \<150 ms |
| `PUT /v1/core/members/me/preferences`  | Runtime    | token de member       | p95 \<300 ms |

## Eventos *(normativo)*

`core.consent.granted` y `core.consent.revoked`, ambos disponibles como webhooks salientes. Una
revocación es de los pocos eventos que un tenant casi siempre quiere espejar en sus propios sistemas.

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

1. Otorgar, revocar y volver a otorgar produce tres filas, y el estado actual es otorgado.
2. Ningún camino de código actualiza ni borra una fila de consentimiento.
3. Una revocación se honra en la siguiente decisión de envío, verificado end to end.
4. Un contacto sin fila de consentimiento no es accionable y no recibe mensajes de marketing.
5. Los mensajes transaccionales llegan a un contacto que revocó marketing.
6. Tras una fusión donde un lado otorgó y el otro revocó el mismo canal y finalidad, el resultado es
   revocado.
7. El conteo de contactos accionables coincide con un conjunto de control calculado
   independientemente sobre una población sembrada de 50 000.
8. **Negativo:** ningún endpoint ni configuración permite inscribir un contacto en marketing por
   defecto.

## Ejecución

Un solo slice, comando síncrono. El predicado de accionable vive en `packages/core` como función
pura, para que medición y mensajería no puedan separarse en su interpretación.

## Preguntas abiertas

| # | Pregunta                                                                                                    | Decide           | Para                            |
| - | ----------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------- |
| 1 | ¿El consentimiento caduca tras un período de inactividad, como sugieren algunas interpretaciones de la ley? | Daniel + abogado | antes del lanzamiento comercial |
| 2 | ¿Versionamos el texto de consentimiento nosotros, o la versión es un string opaco que aporta el tenant?     | 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.*
