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

# Contactos y documentos de identidad

> Cuando esto se entregue, una empresa podrá guardar a sus clientes —personas y empresas por igual— con un documento efectivamente validado para su país, y ese documento quedará enmascarado para todo el que no tenga por qué verlo.

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

## 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
por `contact_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.contacts` con `contact_kind` persona 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 con `legal_name` y 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)*

1. `contact_kind` es `person | company` y 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.
2. Un contacto empresa tiene `legal_name` y **sin `birth_date`**. Los triggers de fecha aplican
   según el tipo: una persona tiene cumpleaños, una empresa tiene aniversario de constitución.
3. El documento se **normaliza antes de persistir** — un normalizador por tipo quita separadores y
   canonicaliza, de modo que `12.345.678-5` y `123456785` son el mismo valor.
4. 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.
5. Donde hay algoritmo de dígito verificador se ejecuta, y `dv_validated` registra si pasó. Donde no
   lo hay, aplica solo la validación de formato y el flag queda en false.
6. 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.
7. 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).
8. 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 en `core.audit_log`** —
   incluidas las exportaciones, registradas como acceso masivo.
9. El documento es opcional por defecto y configurable como obligatorio por tenant (DEC-A5).
10. PROHIBIDO: registrar en logs un documento completo · devolverlo sin enmascarar sin el permiso ·
    guardarlo sin normalizar.

## Datos *(normativo)*

| Tabla           | Invariantes clave                                                                                                                                                                                                                                                                                                      |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.contacts` | `tenant_id` + `cell_id`, RLS obligatoria; `contact_kind` inmutable; `national_id` + `national_id_type` FK, ambos nullable juntos; `dv_validated` BOOL; índice único parcial como arriba; nombres y `birth_date` para personas, `legal_name` para empresas; `email`, `phone`, `locale`, `timezone`; solo borrado lógico |

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)*

| Endpoint                                 | Clase      | Permiso                          | Presupuesto                      |
| ---------------------------------------- | ---------- | -------------------------------- | -------------------------------- |
| `POST /v1/core/contacts`                 | Management | `core.contacts.create`           | p95 \<1 s                        |
| `GET /v1/core/contacts`                  | Management | `core.contacts.read`             | p95 \<1 s, paginación por cursor |
| `GET /v1/core/contacts/{id}`             | Management | `core.contacts.read`             | p95 \<1 s                        |
| `PATCH /v1/core/contacts/{id}`           | Management | `core.contacts.update`           | p95 \<1 s                        |
| `GET /v1/core/contacts/{id}/national-id` | Management | `core.contacts.read_national_id` | p95 \<1 s, auditado              |

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)*

1. Un RUT chileno con dígito verificador incorrecto se rechaza; el mismo con el correcto se guarda
   normalizado, con `dv_validated = true`.
2. Un CPF brasileño y un CUIT argentino validan cada uno contra su propio algoritmo, con fixtures
   válidos e inválidos conocidos.
3. `12.345.678-5` y `123456785` colisionan en el índice único — son el mismo valor tras normalizar.
4. El mismo documento bajo dos tenants distintos se acepta, y ninguno puede leer la fila del otro.
5. Un contacto empresa rechaza `birth_date`; un contacto persona rechaza `legal_name`.
6. `contact_kind` no se puede cambiar después de crearse.
7. Leer un contacto sin `core.contacts.read_national_id` devuelve un valor enmascarado en toda forma
   de respuesta que el módulo expone.
8. Cada llamada al endpoint de valor completo produce exactamente una fila de `core.audit_log`
   nombrando al actor.
9. **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 en `packages/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.

## Preguntas abiertas

| # | Pregunta                                                                                                                          | Decide | Para             |
| - | --------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | ¿El email es único por tenant, o dos contactos pueden compartirlo? Los hogares y las casillas comerciales compartidas son reales. | Daniel | antes de aprobar |
| 2 | ¿Guardamos el teléfono normalizado (E.164) y rechazamos lo demás, replicando la disciplina del documento?                         | 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.*
