Traducción. Autoritativo: fs-core-0002-contacts.md.
Contexto
El contacto es la entidad de la que trata toda la suite. Dos decisiones hacen la nuestra distinta de las herramientas construidas afuera. Un contacto puede ser una empresa (DEC-A1). En Latinoamérica un programa de fidelización o un CRM rutinariamente tienen negocios como clientes —un distribuidor, una clínica, una franquicia— y modelar al cliente como una persona con cumpleaños se rompe apenas aparece una empresa. La división porcontact_kind atraviesa validación, triggers de fecha y campos obligatorios.
El documento es un campo de primera clase, validado y protegido. Es como una empresa realmente
reconoce a su cliente entre sistemas, así que debe normalizarse y validarse con dígito verificador
por tipo (DEC-A3). Es también el campo más sensible de la plataforma, así que se enmascara por
defecto y su valor completo vive tras un permiso dedicado con cada lectura auditada (DEC-A4).
La unicidad es por tenant, nunca global: la misma persona es legítimamente cliente de dos empresas
distintas, y esos dos registros nunca deben verse entre sí.
Alcance (normativo)
core.contactsconcontact_kindpersona o empresa.- Documento: normalización, validación de formato, validación de DV donde exista algoritmo.
- Índice único parcial por (tenant, tipo, documento).
- Enmascaramiento por defecto, lectura completa tras
core.contacts.read_national_id, cada lectura completa auditada. - Configuración de tenant que hace obligatorio el documento.
- Campos base por tipo: persona con nombres y
birth_date; empresa conlegal_namey sin fecha de nacimiento. - CRUD de gestión.
Fuera de alcance (normativo)
- Resolución de identidad y fusiones — FS-CORE-0003.
- Atributos personalizados tipados — FS-CORE-0011.
- Consentimiento — FS-CORE-0004. Que un contacto exista no es un contacto al que se le pueda escribir.
- Importación masiva — FS-CORE-0013.
Comportamiento (normativo)
contact_kindesperson | companyy es inmutable después de crearse. Cambiarlo invalidaría los campos y triggers que dependen de él; la operación correcta es un contacto nuevo.- Un contacto empresa tiene
legal_namey sinbirth_date. Los triggers de fecha aplican según el tipo: una persona tiene cumpleaños, una empresa tiene aniversario de constitución. - El documento se normaliza antes de persistir — un normalizador por tipo quita separadores y
canonicaliza, de modo que
12.345.678-5y123456785son el mismo valor. - El formato se valida contra la regex del tipo, siempre. Un valor que no calza se rechaza, nunca se guarda “para limpiar después”: DEC-A3 existe porque los datos de identidad sucios son irrecuperables a escala.
- Donde hay algoritmo de dígito verificador se ejecuta, y
dv_validatedregistra si pasó. Donde no lo hay, aplica solo la validación de formato y el flag queda en false. - La unicidad es un índice único parcial sobre (tenant_id, national_id_type, national_id) WHERE national_id IS NOT NULL. El mismo documento en tenants distintos siempre se permite.
- La creación directa con documento duplicado se rechaza. La importación masiva enruta los duplicados a una cola de revisión de fusión (DEC-A6, FS-CORE-0013).
- El documento se enmascara por defecto en toda UI y toda respuesta de API. El valor completo
exige
core.contacts.read_national_id, y cada lectura completa escribe encore.audit_log— incluidas las exportaciones, registradas como acceso masivo. - El documento es opcional por defecto y configurable como obligatorio por tenant (DEC-A5).
- PROHIBIDO: registrar en logs un documento completo · devolverlo sin enmascarar sin el permiso · guardarlo sin normalizar.
Datos (normativo)
Sin particionar — los contactos son un conjunto de trabajo, leído constantemente y acotado por el
tamaño del tenant.
Clasificación de datos: el documento es
sensitive; nombres, email y teléfono son personal. Rige
la legislación más estricta entre países soportados (DEC-A4, piso: Ley 21.719 + LGPD).
API (normativo)
El documento completo tiene su propio endpoint en vez de un flag de query. Una ruta separada
hace inequívocos el chequeo de permiso, la entrada de auditoría y el rate limit, y vuelve imposible
una exposición accidental por un serializador compartido.
Eventos (normativo)
core.contact.created y core.contact.updated, ambos disponibles como webhooks salientes. Los
payloads llevan contact_id y los nombres de campos cambiados — nunca el documento,
independiente de la configuración de PII del endpoint.
Criterios de aceptación (normativo)
- Un RUT chileno con dígito verificador incorrecto se rechaza; el mismo con el correcto se guarda
normalizado, con
dv_validated = true. - Un CPF brasileño y un CUIT argentino validan cada uno contra su propio algoritmo, con fixtures válidos e inválidos conocidos.
12.345.678-5y123456785colisionan en el índice único — son el mismo valor tras normalizar.- El mismo documento bajo dos tenants distintos se acepta, y ninguno puede leer la fila del otro.
- Un contacto empresa rechaza
birth_date; un contacto persona rechazalegal_name. contact_kindno se puede cambiar después de crearse.- Leer un contacto sin
core.contacts.read_national_iddevuelve un valor enmascarado en toda forma de respuesta que el módulo expone. - Cada llamada al endpoint de valor completo produce exactamente una fila de
core.audit_lognombrando al actor. - Negativo: ninguna línea de log, payload de evento ni mensaje de error contiene un documento completo — verificado por un barrido sobre un set sembrado de requests.
Ejecución
Un solo slice, comando síncrono. Schema y normalizadores en TS-001; endpoints en el primer slice de API del módulo. Normalizadores y algoritmos de DV viven enpackages/core como funciones puras con
fixtures exhaustivos — es el tipo de código que se escribe una vez y se confía por años, así que se
testea en consecuencia.