Skip to content
leoco/dev
← Volver a proyectos
Full-stack (contribuidor principal, equipo de 2)·2026·Repo privado

Million Box · ERP y tienda web

Monorepo pnpm con un backend NestJS por capas, un frontend Next.js 16 con Tailwind 4, Postgres en Supabase con RLS por sede y pgvector, y un paquete de contratos en Zod que comparten las dos capas. Cuatro roles —Gerencia, Vendedor, Florista y Delivery— entran cada uno a su pantalla. Una comanda recorre el flujo completo: el vendedor la confirma con fecha y franja, la florista la marca lista con foto de salida, Gerencia asigna el delivery y este la entrega con foto o la cierra como no entregada. WhatsApp llega por el Botinfy Gateway con un número por sede, y un agente de OpenAI responde con RAG sobre el catálogo y herramientas que consultan precio y disponibilidad en vivo. La tienda web vive en un repo aparte: guarda el pedido en la API con un código MB-XXXX, abre el WhatsApp de la sede elegida y ofrece cuenta de cliente con código por correo, historial, direcciones y repetir pedido. Se despliega en Coolify con Docker Compose.

Disponible bajo NDA · puedo hacer un walkthrough en una llamada.
STACK TÉCNICO
NestJSNext.js 16SupabasePostgreSQL RLSZodWhatsApp Cloud API
DESAFÍOS

Permisos copiados en cinco sitios

Qué rol puede qué estaba repetido en RLS, los decoradores de Nest, el menú, el middleware de Next y tres listas de roles, y ya había divergido: un florista o un delivery podía leer el CRM de su sede llamando directo a Supabase. Solución: una matriz única en packages/contracts/rbac con puede(), puedeEntrar() e inicioPara(), que leen backend y frontend. El guard de Nest es fail-closed —un endpoint sin acceso declarado responde 403—, RLS queda cubierta por tests pgTAP por rol, y otros tests recorren controllers y páginas para que nada nuevo llegue a deploy sin declarar su acceso.

Dos números, un mismo cliente

Cada sede tiene su número de WhatsApp en coexistencia con la app del teléfono: el mismo cliente puede escribirle a los dos, y el historial llega en bloques de cientos de hilos que no caben en los 5 segundos que espera el gateway. Solución: una tabla de líneas por phone_number_id con su sede, una conversación por contacto y por línea, y la respuesta sale del mismo número vía X-Phone-Number-Id. El historial se guarda crudo y se importa después, retomable si el proceso se cae; la marca de último mensaje solo avanza, porque el gateway no garantiza el orden.

Asignar el delivery sin perder el rastro

Cada sede tiene sus propios deliverys y quien asigna conoce las zonas, así que la asignación no podía ser automática; además hacía falta saber en qué tramo se dañan las flores. Solución: Gerencia asigna viendo a cada delivery como bloqueado, con carga o libre según su turno y la franja, en hora de Caracas. Cada transición es una función de Postgres que revalida el estado dentro de la transacción, con CHECK en la tabla, y dos fotos —salida del taller y entrega— separan la responsabilidad de florista, tienda y delivery.

SOLUCIONES

Contrato primero

Los schemas de Zod viven en packages/contracts: el backend valida cada payload con un ZodValidationPipe y el frontend importa los mismos schemas y tipos. Un cambio de contrato entra en su propia PR antes que la implementación, así que un campo renombrado rompe la compilación de las dos capas en vez de aparecer como bug en staging.

La tienda la configura el ERP

La tienda lee de la API pública las sedes con sus franjas y zonas, el catálogo con caché de 60 s, las formas de pago de la sede o comunes a todas, y los productos que Gerencia oculta por sede. Al guardar, el backend comprueba sede, franja y zona y recalcula el total con los precios del ERP. En lugar de captcha hay un límite de 5 pedidos por minuto por IP y un campo trampa: si viene lleno, responde como éxito sin guardar nada.

Portar en vez de reescribir

El cliente ya había aprobado el prototipo, así que la tienda no se reescribió: un script lo porta y le aplica parches con su motivo, que fallan en voz alta si el prototipo cambia. Un comparador saca capturas de nueve vistas en móvil y escritorio, claro y oscuro, y las compara píxel a píxel; una prueba E2E manda pedidos reales al backend y verifica código, SKU y total.

¿Te interesa este proyecto o tienes una idea similar?