La Punta del Iceberg · Validación técnica

Pipeline de ingesta de eventos: ¿listo para la primera carga?

Verificación contra el código y la base de HOY, no un resumen de lo que se hizo. Incluye una corrida nueva del endpoint con 5 eventos, el contrato documentado y el cierre con lo que bloquea la primera carga.

23-ago-2026 Base de producción consultada Prueba en vivo ejecutada hoy A4 (actividades) fuera de alcance, respetado
⚠ Documento desactualizado

Este documento es una validación ANTERIOR a los ajustes hechos al endpoint de ingesta y ya NO refleja su comportamiento real. No lo uses como referencia para desarrollar el scraper. La referencia vigente es el contrato consolidado: docs/scraping/contrato-endpoint-ingesta-eventos.md (repo LPDI relacionamiento).

Estado REAL verificado hoy en producción (SHA 64192e08) — contradice cuatro afirmaciones de este documento (marcadas abajo con OBSOLETO):

01Tabla de estado

PuntoEstadoEvidencia (hoy)Qué falta
A5
Organizadores
Parcial Directorio: 1.175 activos (el total 1.384 incluye 209 borrados; no creció). Compuestos: de ~89 a 2. Puntuación terminal: 1. Espacios dobles: 0. Tipo dirección: 4. Duplicados por mayúsculas: 3 grupos (6 filas). 47 fusiones ejecutadas. Limpiar residuales (2 compuestos, 4 dirección, 6 duplicados) y 2 vínculos que apuntan a un organizador borrado. Confirmar que las 47 fusiones eran las aprobadas. No encontré tabla de respaldo (ver 06).
A6
Placeholders de ubicación
OK Placeholders de Luma ("registrarse para ver la ubicación"): quedan 2 a 3 (de decenas). El endpoint además los limpia al ingerir (a prueba de futuro). Los eventos con placeholder conservan ciudad, país y coordenadas. Decisión tuya: hay 468 con "Por confirmar" (población distinta, con geo intacta). Parecen sede por definir legítima. Si quieres blanquearlos, lo hago.
B1
Columnas de procedencia
OK Existen fuente, id_fuente, url_fuente. Índice: UNIQUE (fuente, id_fuente) WHERE fuente IS NOT NULL AND id_fuente IS NOT NULL, exactamente como se pidió. Registros con fuente no nula: 0 (limpio, sin pruebas colgadas). Nada.
B2
Endpoint de ingesta
OK Verificado con la prueba en vivo (ver 03): crea, actualiza sin duplicar, rechaza país inválido, limpia placeholder, no crea usuarios. Contrato en el punto 04. Nada para la carga. Opcional: geocodificación (ver 05) y modo de prueba sin escribir.
B3
Aprobación en lote
OK Selección múltiple, procesa todos (en tandas de 6). Sin tope duro: ~150 funciona. Si uno falla, sigue el resto. Aprobar eventos de ingesta NO dispara correos (ya lo dejamos así el 13-ago porque el correo de sistema rebotaría). Nada. Para 150 eventos pasados de CTW: 0 correos.

02Pregunta crítica: eventos con fecha pasada

Sí se puede, sin bloqueo. Confirmado con la prueba en vivo de hoy (el evento "pasado" del lote, fecha 12-ago-2026, se creó normal).
¿Crea evento con fecha pasada?
Sí. El endpoint no compara contra la fecha de hoy. Entra igual que uno futuro.
¿Qué estado le asigna?
PENDING (curación humana), igual que uno futuro.
¿Actualiza uno ya finalizado?
Sí, por el mismo camino de upsert.
¿Falla en silencio?
No. Cada evento devuelve su estado (creado / actualizado / error con motivo).
¿Aparece en el catálogo público al aprobar?
Sí. Un evento pasado se aprueba sin fecha de publicación y queda público de una vez (en la vista de eventos pasados).
Sobre el "solo lectura"
La regla de "evento finalizado es solo lectura salvo super admin" aplica a la EDICIÓN manual en el panel, no a la ingesta ni a la aprobación. Por eso la ingesta de histórico funciona.

03Prueba en vivo · 5 eventos (corrida hoy)

Enviados por el endpoint real contra la base, y borrados al final. Base confirmada como estaba: 0 filas de prueba, 0 usuarios creados.

CasoEnviadoResultadoVerificación en base
1. Válido futurofecha 01-dic-2026, país COcreated P-CO-999-26-00038status PENDING
2. Válido pasadofecha 12-ago-2026, país COcreated P-CO-999-26-00039status PENDING (sin bloqueo por fecha)
3. Reenvío del 1 con cambiomismo id, nombre cambiadoupdated (mismo id y código)1 sola fila, nombre actualizado. No duplicó.
4. País fuera del catálogopaís "XX"error "país no reconocido"No se creó.
5. Placeholder de Luma en ubicación"Registrarse para ver la ubicación"created P-CO-999-26-00040ubicacion_texto quedó en NULL (limpiado).
Usuarios creados durante la prueba: 0. Antes y después: 114 perfiles / 119 cuentas. Borrado confirmado: 0 filas con la fuente de prueba, 0 vínculos huérfanos. Único efecto residual: se consumieron 3 números de consecutivo de evento (no se revierten al borrar). Es inofensivo, solo deja un hueco en la numeración.

04Contrato del endpoint (lo que te desbloquea)

Ruta
POST /api/admin/eventos/ingest
Auth
Authorization: Bearer <INGEST_SECRET> (401 si falta o no coincide)
Cuerpo
JSON. Objeto con fuente (string, aplica a todo el lote) y eventos (array). No es un array plano.
Tope por lote
500 eventos. Sin rate limit. Sin modo de prueba (dry-run): todo lo que entra, escribe.
Requeridos por evento
id_fuente, nombre, fecha_inicio (YYYY-MM-DD)
Coordenadas
ubicacion_lat / ubicacion_lng (numéricas; snake_case)
Procedencia
id_fuente por evento; fuente en el nivel del lote; url_fuente por evento
Campos JSONB
horarios, tematicas, actividades, organizadores_raw, hosts_speakers: van como arrays/objetos, no como string JSON
Deduplicación
Upsert por (fuente, id_fuente). Si existe, ACTUALIZA sin tocar el estado ni el código. Si no, crea en PENDING.
Usuarios
No crea ninguno. Los ingeridos quedan con contact_email = ingesta@lpdi.co y sin usuario dueño.
Filtro ingesta vs persona
fuente IS NOT NULL distingue "lo que entró por ingesta" de lo que registró una persona. (No hay UUID de cuenta de sistema: es un correo fijo, no una cuenta.)
Si un evento falla
Se procesa el resto. Cada uno lleva su propio estado.
Devuelve
Por evento: creado / actualizado / rechazado con motivo, su id_fuente, el evento_id y el evento_codigo generado.

Ejemplo de request (1 evento válido)

curl -X POST https://eco.lpdi.co/api/admin/eventos/ingest \
  -H "Authorization: Bearer $INGEST_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "fuente": "luma",
    "eventos": [{
      "id_fuente": "evt-abc12345",
      "nombre": "Demo Day Colombia Tech Week",
      "fecha_inicio": "2026-08-12",
      "fecha_finalizacion": "2026-08-12",
      "modalidad": "presencial",
      "pais_code": "Colombia",
      "ciudad": "Medellin",
      "estado_provincia": "Antioquia",
      "ubicacion_texto": "Ruta N, Medellin",
      "ubicacion_lat": 6.2447, "ubicacion_lng": -75.5686,
      "organizadores_raw": [{"nombre": "Aceleradora X", "rol": "Aceleradora"}],
      "tematicas": [{"categoria": "innovacion", "subcategoria": "tecnologia", "principal": true}],
      "url_fuente": "https://lu.ma/evt-abc12345"
    }]
  }'

Respuesta

{ "ok": true, "fuente": "luma",
  "summary": { "total": 1, "created": 1, "updated": 0, "errors": 0 },
  "results": [{ "id_fuente": "evt-abc12345", "status": "created",
                "evento_id": "…", "evento_codigo": "P-CO-004-26-00041" }] }
Reglas verificadas contra el código y con prueba real (2026-08-23):
  • pais_code: el endpoint acepta el nombre en español ("Colombia") o el código ISO ("CO"/"COL") y siempre normaliza al nombre. En la columna queda guardado el NOMBRE ("Colombia"), nunca el ISO. Recomendado mandar el nombre para que el payload refleje lo que se guarda. Si el país no está en el catálogo (y no es online), el evento se RECHAZA con motivo.
  • tematicas: la "categoria" no se valida en la ingesta (se guarda cruda). OBSOLETO — ver banner El "subcategoria" del item principal sí decide el segmento de tema del codigo: si no existe en el catálogo, el codigo cae a "999" en silencio y el evento igual se crea. Usar los slugs exactos del catálogo. Ojo con "fintech": el id vigente no tiene código de tema, cae a 999 (pendiente de corregir). OBSOLETO — ver banner
  • estado_provincia, ciudad, organizadores_raw (rol incluido): NO son obligatorios en la ingesta; entran tal cual o vacíos. Los únicos campos que rechazan el evento si faltan son id_fuente, nombre y fecha_inicio (más el país si no es online).
  • Banderas de ubicación (ubicacion_por_confirmar / ubicacion_solo_registrados): HOY el endpoint de ingesta las ignora (entran en false) y guarda lat/lng aunque se manden. Para usarlas por ingesta hay que agregarlas al endpoint (pendiente). OBSOLETO — ver banner

05Geocodificación

El endpoint no geocodifica. OBSOLETO — ver banner Persiste las coordenadas que le mandes; si llega dirección sin lat/lng, quedan en NULL. Hoy, resolverlo es tu tarea antes de enviar.

Tu observación de Luma (las coordenadas solo aparecen cuando el organizador oculta la dirección) va a repetirse en muchas fuentes. Por eso conviene resolverlo en el servidor una sola vez: si el evento llega con ciudad y país pero sin coordenadas, el endpoint podría geocodificar y guardar la aproximación. Así no lo duplicas en cada extractor nuevo. Esfuerzo moderado (conectar un geocodificador y guardar el resultado). Lo dejo como recomendación, no lo toco sin tu visto bueno.

06Cierre: qué falta para ingerir con seguridad

No bloquea la primera carga

  • El endpoint acepta eventos pasados, no crea usuarios, valida país y deduplica. Verificado hoy.
  • La aprobación en lote de eventos de ingesta no dispara correos.
  • El contrato ya está documentado (punto 04): con esto puedes extraer de Luma y enviar.

Puede esperar (o es decisión tuya)

  • Geocodificación en el servidor (recomendado, genérico para todas las fuentes).
  • Residuales de A5: 2 compuestos, 4 tipo dirección, 6 duplicados, 2 vínculos a organizador borrado.
  • Los 468 "Por confirmar" en ubicación: decidir si se blanquean.

Riesgos que señalo (no los preguntaste)

Verificado contra la base y el código de producción del 23-ago-2026. La prueba en vivo se ejecutó y se revirtió por completo. Ninguna decisión previa (incluida A4 fuera de alcance) fue tocada.