George GiosueHablemos
Blog

Cotizador de grupos electrógenos: cinco servicios y una integración con SAP

Configurar un grupo electrógeno no es elegir un producto de un catálogo. Es combinar un motor, un alternador, una integradora, un tablero y una lista larga de opcionales, donde cada combinación tiene su propio derrateo, su precio y sus restricciones de compatibilidad. El punto de partida del proyecto era un conjunto de hojas de cálculo.

El encargo fue reemplazarlas por una aplicación. Terminó siendo cinco servicios, una base de datos de 108 tablas y una integración con SAP que se llevó más tiempo que todo lo demás junto.

Por qué cinco servicios y no uno

No empecé queriendo repartir nada. Cada separación respondió a un motivo concreto, casi siempre a que algo no podía convivir con el resto.

El backend es la aplicación: Express sobre Bun, MySQL, Redis para límites de tasa y caché, Socket.io para avisos en vivo. Ahí vive la lógica de negocio, repartida hoy en 41 controladores.

El frontend es una SPA en React con Vite. PrimeReact para las tablas y formularios densos que pide una herramienta comercial, Tailwind para lo demás, TanStack Query para el estado de servidor y Zustand para el poco estado propio que queda. Está traducido, porque el alcance incluía exportación.

El importador absorbe los catálogos. Es un servicio aparte en Elysia sobre Bun que solo hace una cosa: recibir Excel y dejarlo en la base en un formato usable.

La pasarela del ERP es PHP. Es la única pieza que habla SOAP, y está en PHP precisamente por eso: su cliente SOAP es el que menos fricción da con los descriptores que expone SAP.

La aplicación móvil es Android con Kotlin y Compose, para vendedores en campo.

Todo va en contenedores detrás de un proxy inverso que enruta por etiquetas, en lugar de por una configuración central que haya que tocar en cada despliegue.

Arquitectura del cotizador Los clientes entran por el proxy; el backend concentra la lógica y es lo único que habla con el importador, la pasarela y la capa de datos.

Los catálogos venían pivoteados

Los datos de ingeniería llegaban como los mantiene ingeniería: hojas pensadas para que las lea una persona. Cabeceras en dos filas, celdas fusionadas, la misma marca escrita de varias formas, unidades implícitas en el nombre de la columna.

Nada de eso entra en una tabla relacional sin traducción. Y no era una carga única: los catálogos se actualizan.

Por eso el importador es un servicio y no una función dentro del backend. Procesar un Excel grande tarda, y esa carga no debía competir con las peticiones de cotización en el mismo proceso. Está organizado por el tipo de dato que ingiere —accesorios, combinaciones, datos técnicos— con un orquestador que coordina el orden, porque las combinaciones dependen de que los componentes ya existan.

La decisión que más ahorró después fue no abortar al primer error. Un archivo con cuatro mil filas y doce malas debe cargar las 3.988 y devolver un informe de las doce, con número de fila y motivo. Si falla entero, quien mantiene el catálogo no tiene forma de saber qué corregir.

Carga de catálogo La carga informa del avance mientras procesa, en lugar de dejar la petición colgada.

La integración con el ERP

Esta fue la parte que subestimé.

El ERP era la fuente de verdad de clientes, stock y pedidos, y la integración iba por servicios SOAP: consultar y crear clientes, obtener stock, validar detalle, crear pedidos.

El problema no fue SOAP. Fue que los descriptores definen la forma de los mensajes, no su significado. Qué representa cada código, qué campos son obligatorios en la práctica y qué devuelve el sistema cuando algo va mal son cosas que no viven en un WSDL. En integraciones con sistemas de gestión maduros esto es lo normal, no una excepción.

Lo fui aprendiendo a base de llamadas fallidas:

  • Las fechas viajaban en un formato y los números con la coma y el punto invertidos respecto a los míos.
  • Un campo vacío llegaba unas veces como cadena vacía, otras como nulo y otras ausente. Los tres significaban lo mismo.
  • Los catálogos de códigos no coincidían entre entornos, así que algo validado podía comportarse distinto al promocionarlo.
  • Los mensajes de error eran códigos internos del ERP, sin contexto sobre qué campo los había provocado.

Lo que terminó funcionando fueron tres cosas, ninguna brillante:

Una sola capa que traduce. La pasarela es lo único que sabe que el ERP existe. Convierte a sus formatos en la salida y normaliza en la entrada. El backend hace peticiones REST normales y recibe JSON. Cuando cambió la versión de uno de los servicios, solo hubo que tocar un sitio.

Registrar el intercambio completo. Petición y respuesta, en ambos sentidos. Ocupa, pero fue la única forma de reconstruir qué había pasado cuando algo fallaba en producción y no se podía reproducir.

Validar la respuesta antes de creérsela. Con esquemas de Zod, para enterarme en el borde del sistema y con un mensaje claro, en lugar de tres capas más adentro con un valor indefinido.

Además reuní una colección de peticiones con casos reales, aciertos y errores. Los simulacros que escribí al principio eran demasiado optimistas: siempre devolvían la respuesta bonita. Las respuestas capturadas de verdad son las que enseñan.

Cotizar, de principio a fin

Formulario de creación de cotización Alta de cotización.

El vendedor arma una configuración y el frontend la envía al backend. Ahí se valida el esquema, se resuelven las combinaciones válidas contra el catálogo, se aplican derrateos según las condiciones del sitio de instalación y se calcula el precio con los opcionales y las condiciones comerciales del cliente.

Si hace falta comprobar stock o el cliente es nuevo, entra la pasarela. La cotización se guarda con su detalle y queda registrada en bitácora: quién, cuándo, qué cambió. Esa tabla la puse al principio casi por costumbre y acabó siendo de lo más consultado del sistema, tanto para depurar como para responder preguntas de meses atrás.

Los mismos datos técnicos que alimentan el configurador se publican como fichas consultables sin iniciar sesión. Salió de una petición sencilla —que un vendedor pudiera enviar una ficha a un cliente sin adjuntar un PDF— y como los datos ya estaban normalizados, exponerlos fue barato.

Fichas técnicas públicas Fichas técnicas públicas, generadas desde el mismo catálogo.

Lo que haría distinto

Poner la capa de traducción antes de escribir nada más. Empecé llamando al ERP desde donde hiciera falta y fui centralizando después. Centralizar más tarde costó más de lo que habría costado empezar así.

Interruptores por funcionalidad desde el inicio. Con cinco servicios, publicar algo a medias obliga a coordinar despliegues. Con un interruptor por funcionalidad, el código puede estar desplegado y apagado, y publicar deja de ser un evento.

Escribir las decisiones cuando se toman. El documento de arquitectura llegó tarde y hubo que reconstruir de memoria por qué se habían hecho ciertas cosas. Los mapeos de campos, sobre todo, eran conocimiento que solo existía en el código.

Desconfiar de “es una consulta sencilla”. Cada petición que llegó descrita como sencilla —consultar un cliente, generar un PDF, importar un Excel— terminó siendo de las partes más largas. No por mala fe: la complejidad estaba en detalles que solo aparecen al implementar.

Lo que aguantó

Tres cosas no hubo que revisarlas.

Separar cada servicio por un motivo concreto: la importación por carga, la pasarela por SOAP, la app por plataforma. Cuando la razón del corte es específica, la frontera se sostiene sola.

Dejar lo heredado en un solo sitio. El XML, los formatos y los códigos opacos viven únicamente en la pasarela; el resto del sistema no sabe que el ERP existe.

Registrar desde el primer día. La bitácora y los registros de la pasarela fueron lo que convirtió incidencias imposibles de reproducir en incidencias resolubles.


Servicios

Servicio Stack
Frontend React, Vite, Tailwind, PrimeReact, TanStack Query, Zustand, i18next
Backend Express sobre Bun, MySQL, Redis, Socket.io, Zod, Winston, S3
Importador Elysia sobre Bun, procesamiento de Excel
Pasarela ERP PHP, cliente SOAP, adaptador REST
Móvil Kotlin, Jetpack Compose

Infraestructura: Docker y Traefik. Pruebas: Vitest y Playwright en frontend, Vitest y Supertest en backend.