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

# Ingesta de eventos y tracked events

> Cuando esto se entregue, el punto de venta de un tenant podrá enviar "este cliente pagó la factura 889" y recibir respuesta en menos de 100 ms, con todo lo de aguas abajo ocurriendo en segundos.

> Traducción. Autoritativo: [`fs-core-0006-event-ingestion.md`](/modules/core/features/fs-core-0006-event-ingestion).

## Contexto

Esta es la puerta de entrada de toda la plataforma y el endpoint que toda integración usa primero.
Su latencia es lo primero que mide un desarrollador, y se sitúa dentro del checkout de otra persona,
así que el presupuesto no es negociable.

ADR-013 resuelve la tensión entre "responder rápido" y "hacer mucho trabajo" con fast-ack:
autenticar, validar, limitar, encolar, responder 202. El camino pesado —resolución de identidad,
evaluación de segmentos, matching de reglas— ocurre detrás de la cola. El costo es que las lecturas
son eventualmente consistentes, y el contrato lo dice explícitamente respondiendo 202 y no 200.

Los reintentos están garantizados, porque las redes fallan. La idempotencia no es entonces una
optimización sino la condición de corrección.

## Alcance *(normativo)*

* `POST /v1/core/track`, `/identify` y `/batch` con fast-ack.
* `core.tracked_events`, append-only, particionada RANGE mensual.
* Clave de idempotencia por tenant, deduplicando en la valla de almacenamiento.
* Rate limiting en Upstash, por tenant y por key.
* El procesador: resolución de identidad, append y entrega a evaluación de segmentos y reglas.
* Retención por plan con exportación a Storage antes de soltar una partición.
* `core.event.tracked` en el outbox.

## Fuera de alcance *(normativo)*

* La evaluación de segmentos en sí — FS-CORE-0008.
* El matching de reglas — FS-LOY-0004.
* Write keys públicas para navegador — F1b, con el widget (DEC-D6). F1a es solo server-side.
* Aliases estilo Segment. La ruta canónica es por módulo (DEC-D2).

## Comportamiento *(normativo)*

1. El endpoint hace cinco cosas y ninguna más: autenticar, validar con Zod, limitar, encolar y
   responder **202** con el id del evento. p95 \<100 ms. PROHIBIDA: cualquier lógica de negocio en
   este camino.
2. La respuesta es **202, nunca 200**. El código de estado es el contrato de que el procesamiento
   aún no ocurrió, y es lo que impide que un integrador escriba una suposición de read-after-write.
3. `idempotency_key` es único por tenant. Un duplicado devuelve 202 con el id original y no crea
   **ninguna** segunda fila ni segundo efecto aguas abajo.
4. `occurred_at` lo aporta el llamador con su zona horaria; `received_at` es nuestro. Se guardan
   ambos — un punto de venta que estuvo offline una hora no debe ver sus eventos reordenados.
5. El procesador es idempotente por `core.processed_jobs` **y** por la clave del evento. Dos vallas,
   porque este es el camino donde un duplicado cuesta puntos.
6. La resolución de identidad corre primero: un identificador desconocido crea un contacto anónimo
   en vez de descartar el evento.
7. `tracked_events` es **append-only** y particionada mensualmente. Toda consulta lleva la clave de
   partición — una consulta solo por `contact_id` se rechaza en revisión y por el lint de CI.
8. Las propiedades del evento son JSONB, validadas contra la taxonomía instalada donde aplique
   (FS-CORE-0012) y aceptadas permisivamente donde no.
9. Retención por plan: 13 / 25 / 37+ meses. Una partición se **exporta a Storage como NDJSON
   comprimido antes de soltarse**, nunca simplemente se suelta.
10. Los rate limits se publican por plan (DEC-D4) y se devuelven en cabeceras `RateLimit-*`.

## Datos *(normativo)*

| Tabla                 | Invariantes clave                                                                                                                                                                                                                                                                                                     |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.tracked_events` | append-only; `tenant_id`, `cell_id`, `contact_id`; `event_name`; `properties` JSONB; `occurred_at` (del llamador, con zona) y `received_at` (nuestro); `idempotency_key` único por tenant; `correlation_id` obligatorio; partición RANGE mensual sobre `occurred_at`; la partición DEFAULT alerta ante cualquier fila |

## API *(normativo)*

| Endpoint                 | Clase   | Permiso             | Presupuesto                        |
| ------------------------ | ------- | ------------------- | ---------------------------------- |
| `POST /v1/core/track`    | Runtime | `core.events.write` | **p95 \<100 ms**, 202              |
| `POST /v1/core/identify` | Runtime | `core.events.write` | p95 \<100 ms, 202                  |
| `POST /v1/core/batch`    | Runtime | `core.events.write` | p95 \<200 ms, 202, máx 500 eventos |

## Eventos *(normativo)*

`core.event.tracked` en el outbox después de que el procesador agrega la fila. Disponible como
webhook saliente, que es como un tenant espeja su propio flujo de eventos hacia su warehouse.

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

1. `track` p95 \<100 ms bajo el perfil k6 al throughput objetivo documentado.
2. La misma `idempotency_key` enviada 100 veces produce una fila y un efecto aguas abajo.
3. Eventos con `occurred_at` una hora en el pasado caen en la partición correcta y se ordenan por
   `occurred_at`, no por llegada.
4. Un batch de 500 se acepta; 501 se rechaza con error tipado.
5. Visibilidad del efecto end to end \<5 s p95: evento entra, puntos acreditados, notificación
   encolada.
6. Un test sintético de cambio de mes deja vacía la partición DEFAULT.
7. La exportación de retención produce un NDJSON legible con checksum verificado antes de soltar la
   partición.
8. **Negativo:** una consulta a `tracked_events` sin predicado de tiempo falla el lint de CI.

## Ejecución

Pipeline de eventos asíncrono — el arquetipo canónico. Endpoint en `backend/api`, procesador en
`backend/workers`, jobs de retención y particiones en `backend/scheduler`. Esto es TS-003, y
`trackEvent` (FS-LOY-0004 + TS-004) lo completa.

## Preguntas abiertas

| # | Pregunta                                                                                                                                            | Decide | Para             |
| - | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | ¿El historial de un contacto anónimo sobrevive indefinidamente si nunca se vuelve conocido, o se purga con un reloj más corto? (pregunta 2 del PRD) | Daniel | antes de aprobar |
| 2 | ¿Aceptamos eventos con `occurred_at` en el futuro, y con qué tolerancia?                                                                            | 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.*
