Viernes — 5:42 PM
Seis meses de trabajo finalmente habían llegado a producción.
La integración de pagos no era un feature pequeño. Tocaba directamente el corazón del ERP: el punto de venta.
El acuerdo era salir de forma gradual. Una sucursal primero. Si todo se comportaba bien, después vendrían las demás.
La prueba piloto parecía exitosa. Las transacciones fluían, los logs no mostraban nada extraño y todo se comportaba como esperábamos para un feature nuevo en producción.
Cerré mi laptop convencido de que la parte más difícil ya había quedado atrás.
El lunes por la mañana empezó a sonar mi teléfono.
Un encargado no estaba seguro de si a un cliente realmente se le había cobrado.
El banco mostraba la transacción.
Netpay mostraba la transacción.
El ERP no.
No había huella de una respuesta final. Solo sabíamos que Metrify había enviado la solicitud, que existía un nuevo folio y que la venta no se había cerrado.
En ese momento no pensé que fuera un bug.
Después de tantas pruebas previas, lo primero fue tratarlo como un incidente aislado. Algo incómodo, sí, pero aislado.
El cliente seguía esperando en mostrador. Su aplicación bancaria ya mostraba el cargo. El encargado necesitaba una respuesta.
La pregunta no era técnica.
La pregunta era operativa.
¿El cliente podía llevarse los productos?
Viernes · 5:42 PM
Salida a producción
La integración de pagos salió a producción después de meses de desarrollo, pruebas y validaciones.
Lunes por la mañana
Primera llamada
Una sucursal reportó una venta cobrada en banco y Netpay, pero no cerrada en el ERP.
Martes · 5:00 PM
Segundo incidente
Con más sucursales operando, apareció un caso similar: cliente esperando, banco con cargo y ERP sin cierre.
El despliegue gradual
flowchart LR A["Piloto"] --> B["1 sucursal"] B --> C["3 sucursales"] C --> D["7 sucursales"] D --> E["14 sucursales"] B:::active classDef active fill:#1f2937,stroke:#facc15,color:#ffffff
La primera semana no queríamos probar una idea en abstracto. Queríamos ver cómo se comportaba el flujo en una sucursal real, con cajeros reales, clientes reales y ventas reales.
El problema fue que los incidentes no aparecían de forma ordenada.
No fallaba siempre la misma terminal.
No fallaba siempre la misma sucursal.
No fallaba siempre el mismo punto del flujo.
De mil transacciones podían fallar una o dos.
Pero cuando fallaban, no dejaban suficiente rastro.
Cuando dejó de parecer aislado
El martes instalamos terminales en más sucursales.
A las cinco de la tarde apareció otro caso.
Un cliente estaba en mostrador. El pago aparecía en su aplicación bancaria, pero el ERP seguía mostrando la venta como pendiente.
Otra vez las mismas preguntas.
¿Debía salir el cliente con la mercancía?
¿Debía el cajero cobrar otra vez?
¿O todos debían esperar?
Nadie tenía evidencia suficiente.
Solo supuestos.
Ya no estábamos buscando un bug.
Estábamos intentando saber en qué punto la transacción había dejado de dejar huella.
Con el segundo problema empezamos a revisar todo.
Logs de Apache.
Logs de PHP.
Errores fatales.
Excepciones.
Sockets.
Base de datos.
Nada explicaba el comportamiento.
No era que el sistema estuviera explotando con un error evidente. Era peor. El flujo parecía normal hasta que, en algún punto, simplemente dejábamos de ver la transacción.
El flujo de pago
sequenceDiagram autonumber participant ERP participant Netpay participant Terminal participant Banco ERP->>Netpay: POST Sale Netpay-->>ERP: 200 OK Netpay->>Terminal: Enviar cobro Terminal->>Banco: Autorización Banco-->>Terminal: Aprobado Terminal-->>Netpay: Resultado Netpay-->>ERP: Webhook ERP->>ERP: Cerrar venta
A primera vista, la arquitectura parecía directa.
Una petición. Un gateway. Una terminal. Un banco.
Pero la realidad era distinta.
Un solo pago cruzaba sistemas independientes, cada uno con sus propios tiempos, reintentos, logs, modos de falla y, al final, su propia versión de la verdad.
El problema no era simplemente que algunos pagos fallaran.
El verdadero problema era que nadie podía explicar en qué punto una transacción había desaparecido para el ERP.
Ese descubrimiento cambió el objetivo.
Siete sucursales después
Con siete sucursales operando, el volumen cambió por completo.
Cada sucursal podía tener más de una terminal. Las transacciones ya no ocurrían en serie. Mientras una venta estaba enviando su solicitud, otra podía estar esperando respuesta y otra podía estar recibiendo un webhook.
El log crecía por tiempo, no por transacción.
JSONL stream · 7 sucursales · 2 terminales por sucursal
08:35:10.102 S10-T1 pre-request oDMfPMBz...
08:35:10.448 S03-T2 pre-request C7ai-tOW0...
08:35:11.006 S10-T1 post-response oDMfPMBz...
08:35:11.391 S07-T1 webhook-received L9kaP02m...
08:35:11.884 S02-T2 pre-request aQm91ZxK...
08:35:12.120 S03-T2 post-response C7ai-tOW0...
08:35:12.612 S06-T1 pre-request R81mKQpl...
08:35:13.048 S14-T2 post-response N8xw0Paa...
08:35:13.441 S01-T1 webhook-received kP91aQx2...
08:35:13.982 S10-T1 webhook-missing oDMfPMBz...Eso significaba que una sola venta quedaba mezclada entre cientos de eventos de otras sucursales.
Buscar por monto, cliente, folio o terminal todavía servía en algunos casos, pero ya no era sostenible.
Había momentos en los que teníamos que entrar a datos, revisar tablas y reconstruir la venta manualmente mientras el cliente seguía esperando.
Ahí entendimos que el problema ya no era solamente tener logs.
El problema era no tener contexto.
Necesitábamos otra forma de buscar
No empezamos con un dashboard.
No empezamos con alertas.
No empezamos con una plataforma completa.
Empezamos con algo más pequeño: cada transacción necesitaba una identidad.
Así nació el correlation_id.
No era el héroe de la historia. Era una herramienta de apoyo.
La idea importante era otra: necesitábamos observabilidad aplicada al flujo real de pagos.
La primera evidencia
Evidencia cruda de la transacción
post-response · solicitud de pago aceptada
{
"stage": "post-response",
"method": "POST",
"endpoint": "/gateway/integration-service/transactions/sale",
"http_status": 200,
"timestamp_iso": "2025-10-06T08:39:30-06:00",
"correlation_id": "rTAOeW0rc80QdKWaaHMdk",
"response": {
"code": "00",
"message": "Mensaje enviado exitosamente"
},
"latency_ms": 1639.24
}Desde ese momento ya podíamos identificar mejor cada transacción.
Todavía era manual. Muchas veces era descargar el log, abrirlo, usar Cmd + F y buscar el correlation_id.
Pero ya teníamos algo que antes no existía.
Un punto de partida.
Correlation ID
rTAOeW0rc80QdKWaaHMdk
Stage
post-response
HTTP Status
200 OK
Latency
1639.24 ms
Observed result
El gateway aceptó la solicitud, pero eso no demostraba que el ERP hubiera recibido la confirmación final del pago.
La diferencia importante
Netpay aceptó la solicitud y regresó 200 OK.
Pero el ERP seguía sin confirmación final del pago.
La petición no había fallado.
El estado de la transacción se había vuelto ambiguo.
Failure Mode
Pago ambiguo
La petición fue aceptada por el gateway, pero el ERP nunca recibió el estado final necesario para cerrar la venta.
Operativamente, esto era peor que una falla limpia.
Una petición fallida se puede reintentar.
Un pago ambiguo se tiene que investigar.
Cuando una transacción empezó a tener recorrido
Correlation Journey
rTAOeW0rc80QdKWaaHMdk
POST Request
El ERP envió la solicitud de pago.
Gateway Response
El gateway regresó HTTP 200.
Terminal
El cliente completó el pago.
Webhook
El callback nunca fue recibido.
ERP Update
La venta permaneció pendiente.
El correlation_id no estuvo en todos los sistemas externos.
Pero sí estuvo donde más lo necesitábamos: en nuestra evidencia.
Nos permitió seguir una transacción desde que salía del ERP, comparar sus estados y saber en qué momento dejaba de avanzar para nosotros.
La primera herramienta
Con catorce sucursales operando, buscar manualmente dejó de tener sentido.
Los eventos llegaban mezclados. Las transacciones eran paralelas. El archivo JSONL seguía creciendo.
Necesitábamos una herramienta que recibiera un correlation_id, encontrara todos los eventos relacionados y reconstruyera el recorrido completo.
Así nació el primer analizador.
python analyze.py logs.jsonl —cid rTAOeW0rc80QdKWaaHMdk
Searching…
Found 8 events
Last Stage:
POST Response
Webhook:
NOT FOUND
Diagnosis:
Gateway accepted request.
No callback was received.
Metric
Tiempo promedio de investigación
Before
30 min
After
30 seg
Treinta minutos no era la mejora real.
La mejora real era la confianza.
Por primera vez podíamos responderle a una sucursal con evidencia.
Ya no empezábamos preguntando si era la red, la terminal, el ERP o el proveedor.
Empezábamos siguiendo la transacción.
Lo que realmente construimos
Durante semanas pensé que estábamos estabilizando una integración de pagos.
No era eso.
Estábamos construyendo observabilidad.
La pasarela de pagos fue solamente el problema que nos obligó a hacerlo.
Hoy, cuando una sucursal llama preguntando qué ocurrió con una transacción, ya no empezamos adivinando.
Empezamos trazando.
Y cuando trabajas con dinero, eso cambia todo.
La herramienta
La herramienta terminó publicada como Open Source después de que el problema dejó de ser urgente.
No nació para GitHub.
Nació para producción.
Para que el analizador pudiera reconstruir una transacción, cada evento debía contener, como mínimo:
correlation_idstagetimestamp_isoendpointhttp_statusrequestresponselatency_ms- sucursal, terminal, usuario y folio
Con esos datos era posible reconstruir, en segundos, el recorrido completo de una transacción.
La implementación completa se encuentra disponible como proyecto Open Source.
GitHub
Payment Correlation Analyzer
Herramienta Open Source para analizar archivos JSONL, reconstruir transacciones mediante Correlation ID y generar diagnósticos automáticamente.
La recomendación no es específica para Netpay.
Lo que realmente aprendimos
En sistemas donde circula dinero, el objetivo no es eliminar todos los errores.
El objetivo es que ningún error vuelva a ser un misterio.
Ese día dejamos de construir solamente un ERP.
Empezamos a construir observabilidad.
Temas aprendidos
Este incidente terminó convirtiéndose en la base de varios temas que hoy forman parte de cómo construimos Metrify.
Observabilidad aplicada
Ver el comportamiento real del sistema sin depender de suposiciones.
Correlation ID
Seguir una transacción entre distintos eventos, logs y estados.
Trazabilidad
Reconstruir el recorrido completo de una operación crítica.
JSONL
Registrar eventos estructurados en orden cronológico y fáciles de analizar.
Debugging en producción
Investigar bajo presión usando evidencia, no intuición.
Sistemas distribuidos
Aceptar que ningún sistema posee por sí solo toda la verdad.
Cada uno de estos temas merece su propio artículo.
Pero todos nacieron del mismo problema: necesitábamos dejar de adivinar.