BookinglyTech News
Software

El bug de iFood que costó semanas: comparar code en vez de fullCode

Un SaaS de gestión de restaurantes tardó semanas en hallar por qué ciertos pedidos no llegaban a cocina. El switch miraba un campo que nunca traía el valor esperado.

3 min de lectura2 fuentes contrastadas0 vistas

El equipo de Temperô, un SaaS multi-tenant de gestión de restaurantes, tardó semanas en entender por qué ciertos pedidos de iFood no llegaban a la cocina. La causa estaba en un switch que comparaba un campo que nunca traía el valor esperado. El evento del marketplace llegaba con code igual a "PLC" y fullCode igual a "PLACED"; el código miraba el primero, y todo caía en el default sin dejar rastro.

No es un anuncio, es un caso práctico: la integración con la Order API de iFood corre en Node con TypeScript y Express, sobre Postgres con SQL crudo y migraciones numeradas, sin ORM.

Polling, no webhook

Temperô no eligió el sondeo por gusto: iFood no llama nunca al restaurante, así que el restaurante es el que llama. La API expone un GET a /order/v1.0/events:polling que devuelve los eventos pendientes de un merchant y un POST a /order/v1.0/events/acknowledgment que confirma la recepción y los saca de la cola. El ciclo vive dentro de un setInterval de 15 segundos en el propio proceso Node, protegido por una bandera booleana que evita solapar dos ejecuciones si un tick se pasa de tiempo.

En credenciales usaron el modelo centralizado: un único client_credentials para toda la plataforma, filtrando el merchant con la cabecera x-polling-merchants, en lugar de un OAuth2 por unidad. Eso obligó a refundir a mitad de proyecto un caché de token pensado por unidad en un único caché en memoria. El token se renueva de forma proactiva, con 30 segundos de margen antes del expiresIn, y reactiva ante cualquier respuesta 401.

El v1 cubre solo entrega: el pedido nace con origin='ifood' y dine_type='entrega', y cualquier takeout o dine-in que llegue por el marketplace se rechaza llamando a requestCancellation. Tampoco hay mapeo de carta: el ítem entra con menu_item_id=null y se usan el nombre y el precio del payload. El cobro no necesita operador: cada unidad tiene un usuario de sistema y una caja virtual que abre y cierra sola.

Dos fallos que no hacían ruido

El del code es el más instructivo. Durante semanas el switch comparó contra "PLACED" y "CANCELLED", valores que jamás aparecían en ese campo. Sin excepción, sin log de error, sin nada. El sistema parecía no hacer nada con eventos reales, mientras las pruebas manuales con curl pasaban porque ejercitaban otras partes del flujo. Lo encontraron diseccionando el JSON crudo de un evento guardado en el log, campo por campo, contra la documentación. La corrección fue cambiar event.code por event.fullCode.

En la misma tanda apareció otro: un filtro de fecha en UTC comparado contra hora local sin conversión, que hacía desaparecer de la pantalla de "Pedidos del día" los pedidos de última hora de la noche.

Y quedaba la cancelación asíncrona. El POST de requestCancellation responde 202, que solo significa "recibido". La confirmación real llega después por el mismo polling, como CANCELLED o CANCELLATION_REQUEST_FAILED. La primera versión marcaba el pedido como cancelado en el instante de la llamada, de forma optimista, y eso divergía del estado real de la plataforma.

Los tres fallos comparten patrón. Ninguno rompe nada, ninguno lanza error y los tres se esconden detrás de nombres de campo que parecen obvios. Comparar contra el campo que la documentación marca como canónico, convertir husos horarios de forma explícita y no dar por hecho un 202 son reglas que valen para cualquier integración con una API ajena, no solo para iFood.