2 · Datos y ERP·4 min de lectura

El reproceso que borró el criterio

8 sep 2026Datos y ERPAutomatizaciónMétodo

Un proceso automático volvió a leer su fuente y, sin fallar ni una vez, borró 202 decisiones que una persona ya había tomado.

El código llevaba un comentario que decía, textualmente, que eso nunca pasaría. Lo escribí yo.

Qué hacía el proceso#

Leía un estado de cuenta, extraía los movimientos y los guardaba para que alguien confirmara la contraparte de cada uno. Cada renglón tiene tres cosas que parecen una sola:

  • La evidencia: lo que dice el banco. No se toca nunca.
  • La propuesta: lo que sugiere el diccionario aprendido. Un reproceso puede pisarla.
  • La decisión: lo que una persona confirmó. Un reproceso jamás debe tocarla.

El diseño lo contemplaba. La implementación no.

Por qué las borró#

La escritura usaba \on_conflict\ con \resolution=merge-duplicates\ — un upsert. La idea era "si ya existe, actualízalo".

El problema es qué contenía el objeto que mandaba. Como venía recién leído del PDF, traía \contraparte: null\, \estado: 'pendiente'\, \confirmado_at: null\. Y un upsert reemplaza la fila entera: esos nulos pisaron valores reales.

Un mes pasó de 202 renglones confirmados a cero. Sin un solo error en el log.

Y hubo un segundo error debajo#

¿Por qué reprocesó un mes que ya estaba leído? Porque preguntaba mal.

Para saber qué periodos ya tenía, traía todos de un jalón y armaba un conjunto. PostgREST corta en 1,000 filas aunque pidas 5,000. Con 1,372 movimientos, el conjunto salió incompleto: dos meses no aparecieron, el proceso creyó que faltaban y los volvió a escribir.

Un límite silencioso convertido en una decisión equivocada.

Y un tercero, del intento de arreglarlo#

Al recargar los datos, el primer intento reventó a media carga. Pensamos que no había escrito nada.

PostgREST commitea por lote. Ya había escrito varios de 300 antes de fallar, con una huella distinta a la del arreglo. Enero terminó con 410 filas donde el estado de cuenta tiene 206.

Un fallo a media carga deja basura confirmada. Que el proceso haya abortado no significa que no haya escrito.

Qué hicimos#

El ingestor ya no actualiza. Solo inserta lo que no existe. Pregunta qué huellas hay en ese periodo, inserta las que faltan y no toca ninguna otra. Si algún día hace falta refrescar propuestas, eso es un UPDATE aparte y acotado a los renglones que siguen pendientes.

La pregunta por periodo, no por lote. Una consulta exacta por cuenta y periodo no se puede truncar.

Y la verificación contra la fuente, no contra el resultado del comando. Se borró todo lo del cliente y se volvió a cargar comparando el conteo por mes contra el PDF: 205, 193, 168, 200, 170, 201, 235. Cero huellas repetidas. Después, dos pasadas seguidas del ingestor — la segunda trae cero.

Lo que nos llevamos#

Un upsert no es "actualiza si existe". Es "reemplaza la fila". Si el objeto que mandas trae nulos donde el destino tiene valores, los nulos ganan.

Y lo más incómodo: el comentario en el código afirmaba que la decisión humana estaba protegida. Un comentario describe la intención, no el comportamiento. Lo que protege el juicio de alguien no es la intención de quien escribió el código: es haberlo probado borrándolo a propósito.