Problema
En Cuba, comprar combustible o gas de cocina significa colas físicas en los servicentros, stock incierto y precios que cambian según el formato y el octanaje. Avangenio se propuso poner toda esa transacción por delante de la cola: que el cliente vea qué servicentros cercanos tienen stock de verdad y a qué precio, reserve exactamente lo que necesita, pague, y se presente a retirar contra un código verificable — sin adivinar, sin hacer cola para descubrir que ya no queda nada.
La parte difícil no es la vidriera. Es que una orden no puede ser un simple registro interno: tiene que registrarse contra el sistema de combustible externo sobre el que operan los servicentros, que emite la autorización que hace real un retiro. Así que el producto es un marketplace de consumo montado sobre un contrato de API de terceros, con reglas de identidad (carné de identidad o pasaporte) que deben coincidir exactamente con ese sistema, y una entrega física al final que necesita ser a prueba de manipulación.
La plataforma se llama Muévete y Cocina de puertas adentro: es el nombre que siguen llevando el código, los módulos y la app de repartidores. Lo que llega al cliente son sus marcas — Mercaful, que vende combustible y gas de cocina, y Gasolero, solo combustible. Las pantallas que se muestran aquí son de Mercaful y anteriores a la segunda marca.
Enfoque
Fue un producto de equipo, con uno de los cofundadores de Avangenio como product owner y la dirección a cargo del líder de frontend y un diseñador de producto de la empresa. Trabajé dentro de esa estructura, y lo que aporté varió mucho según la superficie: desde ejecutar la especificación de otro hasta hacerme cargo de una funcionalidad entera, de la tabla en la base de datos a la pantalla.
-
La app de repartidores — mi iniciativa, mi implementación. El equipo necesitaba que el módulo de escáner de QR de la web funcionara offline-first, algo que el navegador resolvía de forma deficiente. Propuse construirlo en cambio como una app Android en Flutter dedicada, tomé un flujo de escáner existente como referencia, y lo reconstruí desde cero en el nuevo stack según mi propio criterio, diseño incluido — se entregó como MyC Dispatcher: la app que usa el repartidor del servicentro para escanear el QR del cliente, verificar la orden y marcarla como entregada. La conduje desde el primer commit hasta el endurecimiento para release — ciclo de vida de la cámara, rechazo de escaneos duplicados a prueba de offline, ProGuard/R8 para que el escáner sobreviva a un build de release, flavors dev/prod, y una purga a los 90 días de los escaneos antiguos.
-
La app web de cliente — contribución sustancial bajo dirección. En la app React Router 7 + Vite fui uno de los ingenieros que ejecutaban las especificaciones del líder y del diseñador, y desarrollé una parte considerable: el marketplace y el mapa en vivo de servicentros, el asistente de orden (octanaje, formato, litros, recogida), el pago y el seguimiento de la orden hasta los estados vacío / error / expirado / reprogramado, y la billetera in-app. También conecté la app web con la integración de pedidos de combustible del equipo de backend, trabajando desde el handoff que me proporcionaron — el camino que registra una orden contra el sistema de combustible externo y convierte la autorización que devuelve en el QR firmado que el cliente presenta al retirar.
-
Control de acceso, hasta el servicentro. Un repartidor solo debería poder confirmar entregas en el servicentro del que responde. Diseñé y construí ese límite en todas las capas: la tabla de asignación repartidor–servicentro, los endpoints que la gestionan y exponen el vínculo en
/auth/me, la regla de API que restringe la confirmación de entrega al servicentro propio, la pestaña de backoffice donde un administrador asigna repartidores, y las pruebas de API que demuestran que el repartidor de un servicentro no puede tocar las órdenes de otro. En el dispositivo, un rechazo por permisos se distingue deliberadamente de una falta de conexión: entre “no tienes permiso” e “inténtalo luego” está la diferencia entre una cola que se detiene y otra que reintenta para siempre.Ese límite tuvo que llegar después al propio escáner. La verificación del QR es offline, así que un QR legítimo de otro servicentro se escanea sin problema y el repartidor descubre que nunca fue suyo cuando la cola se vacía contra el servidor — con el combustible ya entregado. Las dos mitades de la comparación estaban ya en el dispositivo y no se habían encontrado nunca: el QR lleva el servicentro de la orden y
/auth/melleva el del repartidor. Añadílocation_idal payload firmado en la API y el aviso en el momento del escaneo en la app. Como el APK se entrega a mano no hay despliegue coordinado, así que el campo se lee como opcional y la comprobación se activa solo si está presente — la única forma que aguanta cuando el APK que alguien lleva encima es más antiguo que el backend, y la razón por la que advierte en lugar de bloquear. -
Evidencia de entrega — de la base de datos a la pantalla. Con la app de repartidores ya en campo, el producto necesitaba constancia de que la entrega había ocurrido de verdad. La diseñé y la entregué en las tres capas: los endpoints FastAPI que adjuntan fotos a una orden y sacan a la luz las entregas que no tienen ninguna, las pantallas de backoffice donde se revisan, y el flujo de captura en la app de repartidores — hasta tres fotos por confirmación, que después pasaron a ser opcionales. La dificultad no estuvo en la cámara sino en el comportamiento sin conexión: poner en cola las capturas de un dispositivo que pierde señal a mitad de turno, retener las fotos cuando el servidor rechaza la entrega en lugar de descartarlas, distinguir un rechazo por permisos de una simple falta de cobertura, acotar los intentos de subida, y no dejar nunca una foto huérfana en el disco. El repartidor puede además reabrir una orden ya entregada para corregir su evidencia.
-
Aguantar el salto a dos marcas. El equipo convirtió la plataforma en white label para que un mismo código pudiera servir a más de un operador — la segmentación por aplicación cliente, los catálogos por marca, la identidad de correo y los despliegues fueron trabajo de otros ingenieros, no mío. Lo que sí me tocó fue conseguir que mis propias partes aguantaran el salto: sacar la app de repartidores del nombre Mercaful y devolverla a una marca neutral (con los logotipos dibujados como widgets en vez de empaquetados como SVG, para que un futuro cambio de nombre no obligue a rehacer los recursos gráficos en cada densidad de pantalla), hacer que la app de cliente nombre su propio producto según la compilación, y rehacer los documentos legales para un producto de varias marcas: los términos y condiciones y la política de privacidad salen ahora de un único endpoint gobernado desde el backoffice en vez de dos caminos que se habían separado, cada marca puede sobrescribir cualquier documento desde ahí, y las cláusulas compartidas insertan el nombre del sitio y la dirección de contacto, de modo que un mismo texto sirve a todos los operadores sin duplicarlo para cada marca.
-
Auditar mi propia app con ella ya en campo. La app estaba en uso y nada fallaba a la vista, que es justo cuando una superficie de seguridad deja de mirarse. Volví sobre la mía igualmente de cara a la versión 1.3.0. Lo peor de lo que encontré era el bloqueo por PIN: cinco intentos fallidos borraban el hash del PIN y nada más, dejando una pantalla de configuración que no pide prueba alguna de identidad y que entrega la sesión activa a quien elija el nuevo. Fallar el bloqueo era la manera de entrar. El contador, además, vivía en el estado del widget: volver a abrir la app devolvía otros cinco intentos contra un secreto de cuatro dígitos. Ahora vive en almacenamiento seguro y agotarlo borra los tokens — la garantía ya no depende de que yo me acuerde de mirar. En esa misma pasada reordené unas escrituras de token que no eran atómicas, evité que una respuesta vacía de
/public-keysborrara las claves que hacen posible la verificación del QR sin conexión, unifiqué los 401 simultáneos en una sola renovación y saqué los datos del cliente de la copia de seguridad en la nube de Android. -
Publicar la app también es la app. MyC Dispatcher sale como APK directo — sin tienda, sin despliegue por fases: el repartidor instala lo que le entregan. Nada vuelve a firmar la compilación aguas abajo y ningún canal de soporte puede sustituir una clave perdida, así que lo que firme la primera instalación firma todas sus actualizaciones para siempre. Las versiones se firman con un almacén de claves propio, generado por una herramienta que escribí, y las tareas de release fallan al configurarse si faltan las credenciales, de modo que una compilación no puede salir en silencio firmada por otra cosa. En esa misma pasada saqué
x86_64de las versiones de release — 19,5 MB de un APK de 58,7 MB, para un conjunto de dispositivos reales que está vacío — que hasta entonces se descargaba cada repartidor por una conexión cubana. -
Lo que la app le cuenta al operario. La pantalla de inicio mostraba un número a secas bajo “pendientes de sincronizar”: un número que tanto podía significar trabajo por hacer como entregas ya realizadas esperando cobertura. Ahora narra cinco estados sobre las dos colas, con la última sincronización correcta guardada para que sobreviva al reabrir la app, y con el estado sin conexión pintado en neutro y no en rojo de alerta: en una app offline-first estar sin conexión es la situación normal de trabajo, y el rojo le enseña al repartidor que lo esperable es que algo esté roto. El mismo criterio arregló un aviso que hacía creer al repartidor que no podía atender a un cliente adelantado, y puso los colores de la app sobre una escala semántica alineada con el backoffice — con las etiquetas de estado un escalón más oscuras, porque emerald-600 sobre blanco mide 3,77:1, por debajo de AA para un relleno sólido con texto blanco.
-
Testing. Construí la suite end-to-end de Playwright — una estructura Page-Object-Model con selectores solo por
data-testidy fixtures compartidas — cubriendo los flujos de cliente contra el backend real. En la app de repartidores las pruebas sostienen las reglas que a una revisión se le escapan: un test de esquema que fija cada columna, para que colapsar el historial de migraciones no pueda perder ninguna en silencio, y un barrido de todas las pantallas en busca de textos en inglés visibles al usuario — que es como había sobrevividoSin appointment, junto a tres sitios que le mostraban al operario una excepción de Dart en crudo.
Resultado
La plataforma está construida como un producto de dos lados, y las dos marcas están en producción y vendiendo. La contribución que me pertenece por completo es la app de repartidores: un componente para el que identifiqué un mejor enfoque, que propuse y entregué de punta a punta —offline-first, en un stack que seleccioné, con un diseño propio—. En la app web de cliente mi rol fue el de un contribuidor de alto rendimiento, no el del líder: desarrollé una parte considerable de la interfaz y la conecté con la integración del equipo de backend contra el sistema de combustible externo, trabajando bajo el líder de frontend y un diseñador que marcaban la dirección.
Lo que empezó como un rol de frontend se fue ampliando a la vez que el producto. Las piezas posteriores —la evidencia de entrega, el límite de autorización repartidor–servicentro, el sistema de documentos legales— se construyeron tanto en el backend FastAPI y el backoffice como en la app, porque una regla que solo existe en el dispositivo no es una regla. La comprobación de servicentro es el caso más claro: escribí la mitad de la app y la mitad de la API del mismo payload firmado, y lo que exigió pensar no fue ninguna de las dos, sino decidir qué puede dar por hecho un APK que ya está en manos de un repartidor cuando el backend ha seguido avanzando sin él.
La señal más fuerte sobre la plataforma vino de fuera de mi propio trabajo: el equipo la convirtió en white label, y el mismo código sirve hoy a una segunda marca, Gasolero, con su dominio, su paleta y su identidad de soporte propios. Yo no construí esa capa de segmentación — la hicieron otros ingenieros. Pero un código compartido solo se divide limpiamente si cada parte deja de dar por hecha una única marca, y las partes de las que yo respondía —la app de repartidores y los documentos legales— me tocaba a mí sacarlas del nombre Mercaful.
Esa distinción refleja cómo trabajo dentro de un equipo: ejecutar a volumen contra una especificación definida cuando esa es la tarea, y asumir un ownership genuino cuando identifico algo que el equipo debería abordar de otra forma. Asumirlo de verdad también consiste en volver sobre tu propio código a buscar en qué te equivocaste: la manera de saltarse el bloqueo por PIN la escribí yo, y me tocaba a mí encontrarla antes que nadie. En una app que nadie más estaba revisando, esa costumbre es lo único que separa un error de un error publicado.