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

# Importación CSV y cola de revisión de fusiones

> Cuando esto se entregue, una empresa podrá subir años de historial de clientes el primer día, ver exactamente qué pasó con cada fila, y revisar los duplicados que debe decidir una persona.

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

## Contexto

Importar los clientes existentes es lo primero que hace un tenant nuevo y el momento en que el
producto gana o pierde su confianza. Es también la consecuencia directa de nuestra posición de
precio: ADR-014 hace gratis el histórico almacenado precisamente para que importar todo sea la
jugada obvia. Si la importación resulta dolorosa, hemos puesto precio a una acción que hicimos
desagradable.

DEC-A6 fija la regla de seguridad. La creación directa de un documento duplicado se rechaza de
plano, pero una importación masiva no puede fallar 40 000 filas porque 12 parezcan duplicadas. Esas
12 van a una **cola de revisión de fusiones** para una persona, porque una fusión automática con
evidencia débil es el único error que no se puede deshacer.

## Alcance *(normativo)*

* `core.imports`: un job de importación con su archivo, mapeo y estado.
* `core.import_rows`: resultado por fila — creada, actualizada, sugerida, rechazada — con la razón.
* Mapeo de columnas a campos del contacto y a atributos personalizados.
* Dry-run que produce el reporte completo **sin escribir nada**.
* `core.merge_suggestions`: duplicados encolados para revisión con su evidencia.
* Procesamiento reanudable por chunks.
* Reporte de errores descargable como CSV.

## Fuera de alcance *(normativo)*

* Importar eventos. El backfill histórico de eventos es un problema aparte y mayor.
* Sincronización continua desde un sistema externo — eso es una integración, no una importación.
* La UI de carga y revisión — `frontend/console`.
* La fusión automática. Esta feature **sugiere**; FS-CORE-0003 ejecuta, y solo con decisión humana.

## Comportamiento *(normativo)*

1. **Dry-run primero, siempre disponible.** Produce el reporte completo de resultados y no escribe
   nada. Un tenant debería poder ver qué hará una importación antes de que la haga.
2. El procesamiento va por chunks y es **reanudable**. Un archivo de 500 000 filas que falla en la
   300 000 se reanuda ahí; no empieza de nuevo y no duplica las primeras 300 000.
3. Cada fila obtiene un resultado registrado y, cuando no es un éxito simple, una razón. Una fila que
   desapareció en silencio es el modo de falla que destruye la confianza en una importación.
4. Una fila que calza con un contacto existente por un identificador **verificado** lo actualiza. Una
   que calza por una señal más débil crea una **sugerencia de fusión** e importa el contacto por
   separado — nunca una fusión.
5. Los duplicados de `(tipo_documento, documento)` dentro del propio archivo se detectan y encolan,
   no se colapsan en silencio.
6. La validación es por fila: un documento inválido rechaza esa fila, no el archivo.
7. Los contactos importados llegan **sin consentimiento** salvo que el archivo aporte evidencia con
   timestamp de captura. Importar una lista de contactos no es importar permiso para escribirles, y
   aquí es donde ese error se cometería de otro modo.
8. Una importación **nunca se revierte parcialmente**. Las filas exitosas quedan; el reporte dice
   exactamente qué pasó. Deshacer una importación es un restore, no un botón.
9. La tasa de importación se acota por tenant para que una carga masiva no degrade la latencia
   Runtime de nadie.

## Datos *(normativo)*

| Tabla                    | Invariantes clave                                                                                                                                              |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.imports`           | `tenant_id`; `file_ref`; `mapping` JSONB; `status` pending\|dry\_run\|processing\|completed\|failed; contadores; `started_at`, `finished_at`                   |
| `core.import_rows`       | FK importación; `row_number`; `outcome` created\|updated\|suggested\|rejected; `reason`; `contact_id` nullable; append-only; candidata a partición con volumen |
| `core.merge_suggestions` | `tenant_id`; `existing_contact_id`, `incoming_contact_id`; `evidence` JSONB; `status` pending\|merged\|dismissed; `reviewed_by`, `reviewed_at`                 |

## API *(normativo)*

| Endpoint                                       | Clase      | Permiso               | Presupuesto           |
| ---------------------------------------------- | ---------- | --------------------- | --------------------- |
| `POST /v1/core/imports`                        | Management | `core.imports.create` | p95 \<1 s (asíncrono) |
| `GET /v1/core/imports/{id}`                    | Management | `core.imports.read`   | p95 \<1 s             |
| `GET /v1/core/imports/{id}/errors`             | Management | `core.imports.read`   | descarga CSV          |
| `GET /v1/core/merge-suggestions`               | Management | `core.contacts.merge` | p95 \<1 s             |
| `POST /v1/core/merge-suggestions/{id}/resolve` | Management | `core.contacts.merge` | p95 \<1 s             |

## Eventos *(normativo)*

`core.import.completed` en el outbox, para que un tenant dispare su propio seguimiento. Las
creaciones individuales emiten `core.contact.created` como siempre — una importación no es un caso
especial para los consumidores.

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

1. Un dry-run sobre 100 000 filas produce el reporte completo y crea cero contactos.
2. Una importación que falla en la fila 300 000 se reanuda ahí, sin duplicados entre las primeras
   300 000.
3. Cada fila tiene un resultado; los conteos suman exactamente el total de filas del archivo.
4. Una fila que calza por identificador verificado actualiza; una que calza por señal débil crea una
   sugerencia e importa por separado.
5. Los duplicados dentro del propio archivo se encolan, no se colapsan.
6. Los contactos importados no tienen consentimiento salvo que el archivo aporte evidencia con
   timestamp.
7. El CSV de errores hace round-trip: corregirlo y reimportarlo resuelve exactamente esas filas.
8. **Negativo:** ningún camino de importación ejecuta una fusión automática sobre evidencia no
   verificada.

## Ejecución

Pipeline asíncrono. Carga a Storage, procesamiento por chunks en `backend/workers`, idempotente por
chunk vía `core.processed_jobs`.

## Preguntas abiertas

| # | Pregunta                                                                                                                  | Decide           | Para             |
| - | ------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------- |
| 1 | Tamaño máximo de archivo y de filas por importación: ¿dónde ponemos el tope, y es gateado por plan?                       | Daniel           | antes de aprobar |
| 2 | ¿Aceptamos una columna de consentimiento, dada la responsabilidad, o exigimos que se capture por nuestros propios flujos? | Daniel + abogado | 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.*
