Comercio mayorista / B2B / equipos de ventas

Sistema de Cotizaciones con Workflow de Aprobacion

Plataforma B2B para cotizaciones con calculo financiero exacto (Decimal.js), workflow de aprobacion por descuento, conversion automatica a pedido, folios transaccionales y auditoria completa — Next.js 14 + 4 roles.

Next.js 14Prisma 5PostgreSQL 15Decimal.jsAuth.js v5ZodPlaywright
Antes de nosotros

El Reto

Empresas B2B (software, equipos industriales, mayoristas) generan cotizaciones complejas: 5-20 items con descuento por item + descuento global + IVA por item. Hacerlo en Excel es lento y peligroso: un decimal mal sumado puede regalar $50,000 al cliente o perder $30,000 por mes en cuentas grandes. Adicionalmente, vendedores aplican descuentos sin autorizacion del manager y la empresa solo se entera cuando llega el pedido.

El reto financiero era usar aritmetica exacta. JavaScript Number suma 0.1 + 0.2 = 0.30000000000000004 — inaceptable para sistemas que manejan dinero. La solucion exige Decimal.js (precision configurable, redondeo HALF_UP) en TODO calculo: precio con descuento, subtotal, IVA, total. Y NUNCA aceptar totales del body del cliente — el servidor SIEMPRE recalcula.

Adicionalmente: workflow estricto BORRADOR → ENVIADA → APROBADA / RECHAZADA → CONVERTIDA, con limites de descuento por rol (VENDEDOR max 10% global, MANAGER max 30%), folios unicos transaccionales (sin duplicados bajo concurrencia), auditoria completa, y conversion atomica de cotizacion a pedido.

Lo que construimos

La Solucion

Stack Next.js 14 + TypeScript strict + Prisma 5 + PostgreSQL 15. Aritmetica financiera con Decimal.js configurado al inicio: Decimal.set({ precision: 20, rounding: ROUND_HALF_UP }). Cada calculo (calcularItem, calcularCotizacion) en server-side. El cliente puede mandar precios y cantidades, pero los totales se IGNORAN — el servidor siempre recalcula y guarda lo suyo.

Folio transaccional con tabla Sequence + Prisma transaction + SELECT FOR UPDATE: siguienteSecuencia('COT') o ('PED') retorna 'COT-2024-0001' / 'PED-2024-0001' garantizando unicidad incluso bajo 100 requests concurrentes. Workflow con state machine estricta: cada transicion valida estado actual, rol del usuario, y propiedad (VENDEDOR solo retira sus borradores; MANAGER/ADMIN aprueban/rechazan).

Limites de descuento por rol en comercial.policy.ts: si VENDEDOR aplica 15% por item (limite 15% OK) o 12% global (limite 10%, ERROR DESCUENTO_EXCEDIDO 400). Si quiere mas, requiere aprobacion del MANAGER. Conversion a pedido es transaccion: crear Pedido + PedidoItems copiando exactos + cambiar estado a CONVERTIDA. Si falla cualquier paso, rollback total. AuditLog en cada mutacion + cada transicion de estado.

Funcionalidades

Features Clave

Aritmetica Financiera Exacta con Decimal.js

Decimal.js precision 20 + ROUND_HALF_UP en TODO calculo de dinero. JavaScript Number prohibido. Tests unitarios verifican casos limite (0.5 sube, 0.4 baja). Resultados con 4 decimales de precision en BD via Decimal(12,4).

Servidor Recalcula Siempre (RN-01)

El cliente envia { cantidad, precio, descuento }. El servidor IGNORA cualquier subtotal/impuesto/total que mande. Recalcula con Decimal.js y guarda los suyos. Imposible que el front mienta sobre el total.

Folio Unico Transaccional

Tabla Sequence (id, prefijo, ultimo) + transaccion Prisma con SELECT FOR UPDATE. siguienteSecuencia('COT') retorna 'COT-2024-0001' garantizado unico bajo concurrencia. Probado con 100 requests simultaneos en tests.

Workflow Estricto con Maquina de Estados

BORRADOR → ENVIADA → APROBADA / RECHAZADA / VENCIDA → CONVERTIDA. Cada transicion valida estado, rol, propiedad. Transiciones invalidas (BORRADOR → APROBADA) HTTP 400. Cotizacion APROBADA es INMUTABLE — error al editar.

Limites de Descuento por Rol

VENDEDOR: max 15% por item, max 10% global. MANAGER: max 50% por item, max 30% global. ADMIN: sin limite. Si VENDEDOR aplica >limite → 400 con codigo DESCUENTO_REQUIERE_APROBACION. Bloquea guardado.

Vigencia Lazy con Auto-VENCIDA

Cotizacion ENVIADA cuya fechaVigencia < now() pasa automaticamente a VENCIDA al consultarla. Lazy expiry — no requiere cron. La cotizacion VENCIDA no puede aprobarse.

Conversion Atomica a Pedido

POST /cotizaciones/:id/convertir crea Pedido + PedidoItems en transaccion + cambia estado a CONVERTIDA. Si falla cualquier paso, rollback total. Pedido tiene su propio folio (PED-2024-XXXX). Cotizacion ya CONVERTIDA no puede convertirse de nuevo.

Panel de Aprobaciones para Manager

Vista /aprobaciones lista cotizaciones ENVIADA con dias esperando, total, vendedor, cliente. Botones [Aprobar] / [Rechazar] abren modal con motivo. Rechazar requiere motivo min 10 chars.

Screenshots

Como se Ve

Screenshot

Formulario de Cotizacion Multi-seccion

Encabezado (cliente, vigencia, condiciones), tabla editable de items con autocomplete del catalogo, panel de totales fijo a la derecha que se actualiza en tiempo real con Decimal.js. Advertencia visual si descuento excede limite del rol.

Screenshot

Vista Detalle con Timeline + Acciones

Header con folio + cliente + estado badge grande + botones contextuales segun estado/rol. Tabla items solo lectura. Panel totales. Timeline con eventos: 'BORRADOR creado por VendX', 'ENVIADA por VendX', 'APROBADA por MgrY motivo: ...'.

Screenshot

Panel de Aprobaciones (Manager)

Tabla con cotizaciones ENVIADA: folio, cliente, vendedor, total, dias esperando, [Aprobar] [Rechazar]. Click en rechazar abre modal con campo motivo (textarea min 10 chars + contador).

Screenshot

Reporte Pipeline en Funnel

Grafica funnel recharts: cantidad y valor en cada estado (BORRADOR / ENVIADA / APROBADA / CONVERTIDA). KPIs arriba: tasa de aprobacion 30 dias, valor total en pipeline, ticket promedio.

Stack

Detalles Tecnicos

ComponenteTecnologia
FrameworkNext.js 14 (App Router) + TypeScript 5 strict
Aritmetica financieraDecimal.js (precision 20, ROUND_HALF_UP) en TODO calculo de dinero
ORM + DBPrisma 5 + PostgreSQL 15 + Decimal(12,4) y Decimal(14,4) en monetarios
AuthAuth.js v5 + Argon2id + 4 roles (ADMIN / VENDEDOR / MANAGER / VIEWER)
ValidacionZod 3 .strict() (rechaza campos extra) + react-hook-form
Folios transaccionalesSequence table + Prisma transaction + SELECT FOR UPDATE
Rate limitingrate-limiter-flexible + Redis (login 5/15min, cotizaciones 20/min)
Reportesrecharts (funnel pipeline) + papaparse (CSV)
TestingVitest (calculo + policy) + integration (workflow) + Playwright E2E
Loggingpino structured
Metricas reales

Resultados

6 (BORRADOR/ENVIADA/APROBADA/RECHAZADA/VENCIDA/CONVERTIDA)

Estados de cotizacion

4 (ADMIN/VENDEDOR/MANAGER/VIEWER)

Roles con permisos diferenciados

14 (RN-01 a RN-14)

Reglas de negocio testeadas

Decimal.js (cero flotantes)

Aritmetica financiera

Si — transaccion + FOR UPDATE

Folio unico bajo concurrencia

Atomica (rollback si falla)

Conversion a pedido

¿Tu equipo de ventas cotiza en Excel y a veces se equivocan en el total?

Sistema con calculo financiero exacto, workflow de aprobacion por descuento, folios transaccionales y conversion atomica a pedido. Sin Excel, sin sorpresas.