Visión general de la arquitectura
DAVAL App es una SPA de React que consume una API REST en Express. Toda regla de negocio que afecte dinero o permisos (precio final, ventana de corte, alcance por rol) se decide en el servidor; el frontend solo muestra.
Diagrama de componentes
┌──────────────────────────────────────────┐
│ Navegador │
│ React SPA (Vite) │
│ ├─ AuthContext (sesión, tokens) │
│ ├─ AppContext (carrito) │
│ └─ React Query (datos del servidor) │
└──────────────┬───────────────────────────┘
│ HTTPS /api/* Bearer JWT
▼
┌──────────────────────────────────────────┐
│ API Express (Node) │
│ middleware: cors → json → auth → zod │
│ routes/ → lib/ (reglas) → services/ │
└───┬──────────────┬──────────────┬────────┘
│ pg.Pool │ S3 API │ HTTPS
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌──────────────┐
│ PostgreSQL │ │ Supabase │ │ SIIGO API │
│ (Supabase) │ │ Storage │ │ (ERP) │
└────────────┘ └──────────────┘ └──────────────┘
▲
SMTP ────────┘ (Nodemailer, correos)
Despliegue en producción
En producción el frontend y la API comparten dominio en Vercel:
vercel.jsonconstruye el frontend (npm run build→dist/).- Toda petición a
/api/*se reescribe a la función serverlessapi/index.js, que re-exporta la app Express deapi/src/app.js. - Cualquier otra ruta se reescribe a
index.html(routing del lado del cliente).
Como alternativa, la API puede correr como proceso Node en Railway o Render (node api/src/index.js). Ver Despliegue.
Flujo principal: un cliente envía una cotización
Cliente Frontend API DB
│ abre /cliente │ │ │
│ ─────────────────────► │ GET /api/routes/me/cutoff │ │
│ │ ───────────────────────────► │ computeRouteCutoff() │
│ │ ◄─ { isOpen, deadline… } ─── │ │
│ navega catálogo │ GET /api/products │ │
│ │ ───────────────────────────► │ resolvePrices() ─────────► │
│ │ ◄─ items con finalPrice ──── │ │
│ agrega al carrito │ (AppContext + localStorage) │ │
│ confirma │ POST /api/quotations │ │
│ │ { branchId, items[] } │ 1. valida corte (422) │
│ │ ───────────────────────────► │ 2. recalcula precios │
│ │ │ 3. TX: quotation + items ► │
│ │ ◄─ 201 cotización completa ─ │ │
El frontend nunca envía precios: solo productId y quantity. El servidor recalcula todo.
Flujo: la cotización llega a SIIGO
Admin ── POST /api/integrations/siigo/quotes/:id ──► API
├─ valida ítems, NIT, productos sincronizados
├─ valida cliente en SIIGO (por NIT)
├─ resuelve vendedor (siigo_seller_id del asesor)
├─ registra intento en siigo_quote_integrations
├─ POST /v1/quotations en SIIGO (reintentos 429/5xx)
└─ TX: integración = success, cotización = synced
Detalle completo en Integración SIIGO.
Principios de diseño
- El servidor es la autoridad. Precio final (
min(promoción, lista)), ventana de corte y alcance por rol se calculan en la API. El frontend replica el cálculo del corte solo para mostrar mensajes. - Validación con Zod en cada entrada. Un
ZodErrorse convierte automáticamente en400 VALIDATION_ERROR. - Errores con forma única.
{ error, message, details? }en todas las respuestas de error. - Transacciones para escrituras compuestas. Cotización + ítems, listas de precios, promociones y creación de usuario-cliente usan
BEGIN/COMMIT. - Efectos secundarios no bloqueantes. Correos y borrado de imágenes viejas se disparan fire-and-forget después de responder.
- Compatibilidad serverless. Sin disco persistente (uploads a memoria → Supabase Storage), pool de conexiones reducido y reseteo de sincronizaciones colgadas en la primera petición.