> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coupons.piixan.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Carga de histórico

> El modelo canónico de origen: las ocho tablas que pedimos a cualquier CRM y cómo se transforman en eventos v1.

Para la carga inicial (y para conectores por volcado) no hace falta producir
eventos v1 a mano: basta con entregar los datos en el **modelo canónico**, ocho
tablas con columnas fijas, y ejecutar `conector-canonico`, que hace la
transformación, seudonimiza los clientes y deja un fichero de eventos listo para
el [validador](/guias/validador).

## Convenciones

* Un fichero por tabla, CSV (UTF-8, cabecera, vacío = nulo) o Parquet, con el
  nombre de la tabla: `tickets.csv`, `lineas_ticket.parquet`…
* **Fechas en UTC.** ISO-8601 con `Z` u offset; si vienen sin zona se interpretan
  como UTC (o la zona que indiques con `--zona-naive`).
* **Dinero en MXN sin IVA**, salvo `tickets.total`, que es el total impreso en el
  ticket (con IVA) y sirve para comprobar el IVA.
* `cliente_id` es el identificador interno del CRM. El conector lo seudonimiza con
  una sal que guardas tú; ningún identificador en claro llega al motor.
* Solo se admiten las columnas listadas; una columna desconocida detiene la carga.

## Tablas

### `clientes`

Un registro por cliente identificable en el CRM.

| columna              | tipo      | obligatoria | descripción                                                    |
| -------------------- | --------- | ----------- | -------------------------------------------------------------- |
| `cliente_id` (clave) | texto     | sí          | Identificador interno del CRM (nunca un correo ni un teléfono) |
| `fecha_alta`         | fecha UTC | no          | Cuándo se dio de alta; genera el evento ALTA                   |
| `canal_alta`         | texto     | no          | tienda, web, app…                                              |
| `segmento`           | texto     | no          | Segmento declarado por el CRM, si existe                       |
| `perfil`             | texto     | no          | Perfil declarado, si existe                                    |

```csv theme={null}
cliente_id,fecha_alta,canal_alta,segmento,perfil
1001,2026-01-10T15:00:00Z,app,familia,
1002,,,,
```

### `tickets`

Una fila por ticket de venta.

| columna             | tipo        | obligatoria | descripción                                                       |
| ------------------- | ----------- | ----------- | ----------------------------------------------------------------- |
| `ticket_id` (clave) | texto       | sí          | Identificador único del ticket                                    |
| `cliente_id`        | texto       | sí          | Cliente al que se atribuye la compra                              |
| `ts`                | fecha UTC   | sí          | Fecha y hora de la venta, en UTC                                  |
| `tienda`            | texto       | no          | Tienda o punto de venta                                           |
| `canal`             | texto       | no          | tienda, web, app…                                                 |
| `total`             | MXN sin IVA | no          | Total impreso en el ticket (con IVA); sirve para comprobar el IVA |

```csv theme={null}
ticket_id,cliente_id,ts,tienda,canal,total
T-88213,1001,2026-03-04T18:22:10Z,suc-014,tienda,612.40
```

### `lineas_ticket`

Una fila por línea de ticket. El margen sale de `margen`, de `importe − coste` o del coste estándar del artículo, en ese orden.

| columna             | tipo        | obligatoria | descripción                                                                       |
| ------------------- | ----------- | ----------- | --------------------------------------------------------------------------------- |
| `ticket_id` (clave) | texto       | sí          |                                                                                   |
| `linea` (clave)     | entero      | sí          | Número de línea dentro del ticket                                                 |
| `articulo_id`       | texto       | sí          |                                                                                   |
| `cantidad`          | número      | sí          | Unidades (o kilos)                                                                |
| `importe`           | MXN sin IVA | sí          | Importe de la línea sin IVA, antes del descuento de cupón (que va en redenciones) |
| `margen`            | MXN sin IVA | no          | Margen de la línea sin IVA antes del descuento de cupón; si falta, se calcula     |
| `coste`             | MXN sin IVA | no          | Coste de la línea sin IVA; se usa si falta `margen`                               |

```csv theme={null}
ticket_id,linea,articulo_id,cantidad,importe,margen,coste
T-88213,1,A-771,2,120.00,31.50,
T-88213,2,A-905,1,66.21,,52.00
```

### `articulos`

Catálogo de artículos con su categoría (la unidad de agregación de las compras).

| columna               | tipo        | obligatoria | descripción                                                        |
| --------------------- | ----------- | ----------- | ------------------------------------------------------------------ |
| `articulo_id` (clave) | texto       | sí          |                                                                    |
| `categoria`           | texto       | sí          | Categoría del catálogo del inquilino; es la `clave` de COMPRA\_CAT |
| `subcategoria`        | texto       | no          |                                                                    |
| `marca`               | texto       | no          |                                                                    |
| `coste_estandar`      | MXN sin IVA | no          | Coste unitario sin IVA; se usa si la línea no trae margen ni coste |

```csv theme={null}
articulo_id,categoria,subcategoria,marca,coste_estandar
A-771,lacteos,yogur,,
A-905,panaderia,,,48.00
```

### `campanas` (opcional)

Campañas de cupones (opcional; da contexto a los envíos).

| columna              | tipo      | obligatoria | descripción |
| -------------------- | --------- | ----------- | ----------- |
| `campana_id` (clave) | texto     | sí          |             |
| `fecha_envio`        | fecha UTC | no          |             |
| `canal`              | texto     | no          |             |
| `descripcion`        | texto     | no          |             |

```csv theme={null}
campana_id,fecha_envio,canal,descripcion
camp-2026-10,2026-03-06T15:00:00Z,push,Lácteos marzo
```

### `cupones_emitidos`

Un cupón enviado a un cliente. Genera CUPON\_RECIBIDO y, si vence sin redención, CUPON\_CADUCADO.

| columna                    | tipo        | obligatoria | descripción                                          |
| -------------------------- | ----------- | ----------- | ---------------------------------------------------- |
| `cupon_emitido_id` (clave) | texto       | sí          | Identificador único del envío (no del tipo de cupón) |
| `cliente_id`               | texto       | sí          |                                                      |
| `cupon_tipo_id`            | texto       | sí          | Tipo de cupón; es la `clave` de los eventos de cupón |
| `ts_envio`                 | fecha UTC   | sí          |                                                      |
| `caducidad`                | fecha UTC   | sí          |                                                      |
| `campana_id`               | texto       | no          |                                                      |
| `canal`                    | texto       | no          | sms, email, push, app, ticket…                       |
| `descuento_pct`            | número      | no          | Porcentaje de descuento, si es porcentual            |
| `valor_max`                | MXN sin IVA | no          | Descuento máximo en MXN, si es un importe            |
| `decision_id`              | texto       | no          | Solo cuando el cupón lo decidió el motor             |

```csv theme={null}
cupon_emitido_id,cliente_id,cupon_tipo_id,ts_envio,caducidad,campana_id,canal,descuento_pct,valor_max,decision_id
cup-55019,1001,lacteos_15,2026-03-06T15:00:00Z,2026-03-20T05:59:59Z,camp-2026-10,push,15,,
```

### `redenciones`

Un cupón usado en un ticket.

| columna                    | tipo        | obligatoria | descripción                                         |
| -------------------------- | ----------- | ----------- | --------------------------------------------------- |
| `cupon_emitido_id` (clave) | texto       | sí          |                                                     |
| `ticket_id` (clave)        | texto       | sí          |                                                     |
| `ts`                       | fecha UTC   | sí          | Momento de la redención (normalmente el del ticket) |
| `descuento`                | MXN sin IVA | sí          | Descuento aplicado sin IVA                          |

```csv theme={null}
cupon_emitido_id,ticket_id,ts,descuento
cup-55019,T-88420,2026-03-09T19:40:00Z,27.93
```

### `bajas` (opcional)

Bajas de comunicaciones o de cuenta.

| columna              | tipo      | obligatoria | descripción                                          |
| -------------------- | --------- | ----------- | ---------------------------------------------------- |
| `cliente_id` (clave) | texto     | sí          |                                                      |
| `ts` (clave)         | fecha UTC | sí          |                                                      |
| `clave` (clave)      | texto     | sí          | `comunicaciones` (penaliza la recompensa) o `cuenta` |
| `canal`              | texto     | no          | Canal del que se dio de baja, si aplica              |

```csv theme={null}
cliente_id,ts,clave,canal
1002,2026-03-21T14:05:00Z,comunicaciones,push
```

## Transformar y validar

```sh theme={null}
export SAL_SEUDONIMIZACION='una-sal-larga-que-solo-tú-guardas'
conector-canonico ./canonico --salida eventos.jsonl --validar
```

Opciones: `--zona-naive America/Mexico_City` para fechas sin zona en hora local;
`--margen-desconocido cero|pct:30` si hay líneas sin margen, coste ni coste
estándar (por defecto se detiene); `--sin-caducados` para no sintetizar
`CUPON_CADUCADO` a partir de los cupones vencidos sin redención.

<Warning>
  Guarda la sal fuera del repositorio y no la pierdas: con otra sal, los mismos
  clientes tendrían otro `cliente_ref` y las cargas futuras no se enlazarían.
</Warning>

## Si prefieres tablas en Postgres

El mismo modelo como DDL, para llenarlo con vistas o `INSERT` desde tu CRM y
exportarlo después a CSV o Parquet:

<Accordion title="esquema.sql">
  ```sql theme={null}
  -- Modelo canónico de origen (canonico-v1). Generado por conectores.canonico.esquema.
  create schema if not exists canonico;

  create table if not exists canonico.clientes (
      cliente_id text not null,
      fecha_alta timestamptz,
      canal_alta text,
      segmento text,
      perfil text,
      primary key (cliente_id)
  );
  comment on table canonico.clientes is 'Un registro por cliente identificable en el CRM.';

  create table if not exists canonico.tickets (
      ticket_id text not null,
      cliente_id text not null,
      ts timestamptz not null,
      tienda text,
      canal text,
      total numeric(14,2),
      primary key (ticket_id),
      foreign key (cliente_id) references canonico.clientes (cliente_id)
  );
  comment on table canonico.tickets is 'Una fila por ticket de venta.';

  create table if not exists canonico.lineas_ticket (
      ticket_id text not null,
      linea bigint not null,
      articulo_id text not null,
      cantidad numeric(14,2) not null,
      importe numeric(14,2) not null,
      margen numeric(14,2),
      coste numeric(14,2),
      primary key (ticket_id, linea),
      foreign key (ticket_id) references canonico.tickets (ticket_id),
      foreign key (articulo_id) references canonico.articulos (articulo_id)
  );
  comment on table canonico.lineas_ticket is 'Una fila por línea de ticket. El margen sale de `margen`, de `importe − coste` o del coste estándar del artículo, en ese orden.';

  create table if not exists canonico.articulos (
      articulo_id text not null,
      categoria text not null,
      subcategoria text,
      marca text,
      coste_estandar numeric(14,2),
      primary key (articulo_id)
  );
  comment on table canonico.articulos is 'Catálogo de artículos con su categoría (la unidad de agregación de las compras).';

  create table if not exists canonico.campanas (
      campana_id text not null,
      fecha_envio timestamptz,
      canal text,
      descripcion text,
      primary key (campana_id)
  );
  comment on table canonico.campanas is 'Campañas de cupones (opcional; da contexto a los envíos).';

  create table if not exists canonico.cupones_emitidos (
      cupon_emitido_id text not null,
      cliente_id text not null,
      cupon_tipo_id text not null,
      ts_envio timestamptz not null,
      caducidad timestamptz not null,
      campana_id text,
      canal text,
      descuento_pct numeric(14,2),
      valor_max numeric(14,2),
      decision_id text,
      primary key (cupon_emitido_id),
      foreign key (cliente_id) references canonico.clientes (cliente_id),
      foreign key (campana_id) references canonico.campanas (campana_id)
  );
  comment on table canonico.cupones_emitidos is 'Un cupón enviado a un cliente. Genera CUPON_RECIBIDO y, si vence sin redención, CUPON_CADUCADO.';

  create table if not exists canonico.redenciones (
      cupon_emitido_id text not null,
      ticket_id text not null,
      ts timestamptz not null,
      descuento numeric(14,2) not null,
      primary key (cupon_emitido_id, ticket_id),
      foreign key (cupon_emitido_id) references canonico.cupones_emitidos (cupon_emitido_id),
      foreign key (ticket_id) references canonico.tickets (ticket_id)
  );
  comment on table canonico.redenciones is 'Un cupón usado en un ticket.';

  create table if not exists canonico.bajas (
      cliente_id text not null,
      ts timestamptz not null,
      clave text not null,
      canal text,
      primary key (cliente_id, ts, clave),
      foreign key (cliente_id) references canonico.clientes (cliente_id)
  );
  comment on table canonico.bajas is 'Bajas de comunicaciones o de cuenta.';
  ```
</Accordion>
