Traducción. Autoritativo: fs-core-0004-consents.md.
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 digaemail_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.
messaginglee el consentimiento; la decisión vive enresolveChannels. - 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)
- 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.
- 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.
- 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. - La finalidad transaccional no es opt-out (DEC-E3). Quien compró algo recibe su comprobante. Solo una supresión por rebote duro lo detiene.
- La ausencia de consentimiento no es consentimiento. El estado por defecto para marketing es denegado, en todas partes, sin override por tenant.
- Una revocación surte efecto de inmediato — antes de la siguiente decisión de envío, no en el recálculo nocturno.
- 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.
- 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.
- 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)
API (normativo)
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)
- Otorgar, revocar y volver a otorgar produce tres filas, y el estado actual es otorgado.
- Ningún camino de código actualiza ni borra una fila de consentimiento.
- Una revocación se honra en la siguiente decisión de envío, verificado end to end.
- Un contacto sin fila de consentimiento no es accionable y no recibe mensajes de marketing.
- Los mensajes transaccionales llegan a un contacto que revocó marketing.
- Tras una fusión donde un lado otorgó y el otro revocó el mismo canal y finalidad, el resultado es revocado.
- El conteo de contactos accionables coincide con un conjunto de control calculado independientemente sobre una población sembrada de 50 000.
- 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 enpackages/core como función
pura, para que medición y mensajería no puedan separarse en su interpretación.