Saltar al contenido
PG
Todos los proyectos

WarehouseProject

ERP ligero para una tienda o un almacén: pedidos que descuentan stock de forma atómica, facturas con IVA en PDF y tres roles con permisos.

En 30 segundos

Categoría
Full stack · Inventario y ventas
Estado
Completo · sin demo pública
Stack
Node.js · Express 5 · Prisma · PostgreSQL 16 · React 19
Tests
36 de integración contra PostgreSQL real, incluidos de concurrencia
Seguridad
JWT con refresh rotado · 3 roles validados en el backend
Entrega
Docker Compose · CI en GitHub Actions · despliegue preparado para Render y Vercel
Demo online
Sin demo pública

Visión general

Qué es
Una aplicación web completa (API REST y SPA) para gestionar el catálogo, el inventario, los clientes, los pedidos y la facturación de una tienda o un almacén pequeño.
Qué problema resuelve
En un negocio pequeño, el stock y la facturación suelen vivir en hojas de cálculo que no aguantan dos personas trabajando a la vez: se vende lo que ya no hay y no se sabe por qué cambió una cifra.
Para quién
Un equipo pequeño con tres perfiles: administración, comerciales que crean pedidos y almacén que registra entradas y salidas.
Objetivo
Que el stock sea siempre explicable y correcto aunque varias personas trabajen a la vez, y que cada rol vea y haga solo lo que le corresponde.

Vista previa

warehouseproject · vista previa
Datos de demostración. Resumen de WarehouseProject: ventas e ingresos del mes, importe pendiente de cobro, gráfica de ventas de seis meses, productos más vendidos y productos con stock bajo.
Datos de demostración. El mismo resumen en un móvil de 390 px: las tarjetas se apilan y la gráfica ocupa todo el ancho.

Funcionalidades

Implementado

Pedidos con descuento de stock transaccional

Un pedido pasa por borrador, confirmado, pagado o cancelado. Al confirmarlo se descuenta el stock de todas sus líneas en una sola transacción; al cancelarlo se devuelve.

Para qué sirve: Si a una línea le falta stock no se confirma nada, así que nunca queda un pedido a medias.

Datos de demostración. Detalle de un pedido confirmado con sus líneas, precios unitarios guardados y total.
Implementado

Libro de movimientos de stock

Cada entrada o salida queda registrada: reposiciones, mermas, ajustes y las salidas que generan los pedidos.

Para qué sirve: El stock actual siempre se puede explicar mirando su historial.

Datos de demostración. Movimientos de stock de un producto: entradas y salidas con fecha, tipo, cantidad y pedido de origen.
Implementado

Facturas con IVA, PDF y vencimientos

Las facturas se emiten desde un pedido, guardan base, IVA y total, se descargan en PDF y pasan solas a vencidas cuando se supera la fecha de cobro.

Para qué sirve: Los importes de una factura no cambian aunque luego cambien los precios o el IVA.

Datos de demostración. Factura emitida con base imponible, IVA, total, estado y botón para descargar el PDF.
Implementado

Tres roles con permisos por recurso

ADMIN, COMERCIAL y ALMACEN pueden consultar todo, pero cada rol solo escribe en lo que le toca. El backend lo valida siempre; el frontend oculta lo que no puedes hacer.

Para qué sirve: Nadie puede saltarse un permiso llamando a la API directamente.

Datos de demostración. Listado de productos con búsqueda, filtros por categoría y estado, paginación y stock de cada producto.
Implementado

Diseño adaptado al móvil

En pantallas estrechas las columnas secundarias se ocultan y lo esencial pasa debajo del dato principal. Revisado a mano en 360, 768, 1024, 1280 y 1440 px.

Para qué sirve: El almacén puede registrar movimientos desde el móvil.

Datos de demostración. Listado de pedidos en el móvil, con el cliente y el estado debajo del número de pedido.
Implementado

Panel con métricas del negocio

Ventas e ingresos del mes, pendiente de cobro, ventas de los últimos seis meses, productos más vendidos, stock bajo y pedidos por estado.

Para qué sirve: Una vista rápida de cómo va el negocio sin exportar nada.

Stack

Backend

Node.js + Express 5
API REST
TypeScript (strict)
Tipado en todo el proyecto
Zod
Validación de peticiones y de variables de entorno
jsonwebtoken + bcryptjs
Autenticación
helmet · express-rate-limit · cors
Cabeceras seguras y límite de intentos de login
pdfkit
PDF de facturas

Base de datos

PostgreSQL 16
Datos relacionales, importes en NUMERIC
Prisma 6
Esquema, migraciones y acceso a datos

Frontend

React 19 + Vite
SPA con carga de páginas bajo demanda
TanStack Query
Datos del servidor y caché
React Hook Form + Zod
Formularios
Tailwind CSS 4
Estilos
Recharts
Gráficas del resumen

Testing

Jest + Supertest
Tests de integración contra PostgreSQL
Vitest
Lógica pura del frontend

Infraestructura

Docker + Docker Compose
PostgreSQL, API y frontend con un comando
nginx
Sirve la SPA y reenvía /api
GitHub Actions
Lint, formato, tipos, tests, build e imágenes
Render + Vercel
Despliegue preparado (blueprint y configuración)

Arquitectura

Monorepo con dos proyectos independientes. La API sigue una arquitectura por capas en la que cada capa tiene una sola responsabilidad, y el service decide qué operaciones van juntas en una transacción.

Arquitectura de WarehouseProjectLa SPA de React llama a nginx, que reenvía /api a la API Express. La API pasa cada petición por rutas, controller, service y repository, y el repository usa Prisma para hablar con PostgreSQL.ClienteSPA React 19TanStack Query · RouterHTTPEdgenginxestáticos + proxy /api/apiAPIExpress 5JWT · roles · Zod · erroresroutescontrollerservicerepository → PrismaSQL · transaccionesDatosPostgreSQL 16NUMERIC · advisory locks
  • Las rutas solo encadenan middlewares (autenticación, rol, validación); el controller traduce HTTP; el service tiene la lógica de negocio y abre las transacciones; el repository es el único que habla con Prisma.
  • Los repositories reciben el cliente de la transacción, así que el mismo método funciona dentro o fuera de una transacción sin saberlo.
  • Todos los errores acaban en un único middleware: Zod → 400, negocio → 404/409, errores de Prisma traducidos y nunca un stacktrace.
  • En Docker, nginx sirve la SPA y reenvía /api a la API, que aplica las migraciones al arrancar.

Decisiones técnicas

Cada decisión con el problema, la solución, el motivo y el lugar exacto del código donde está.

Stock sin condiciones de carrera

Problema
Dos comerciales confirman a la vez un pedido con la última unidad de un producto. Si se lee el stock y luego se resta, los dos ven una unidad disponible y el stock acaba en −1.
Solución
Un único UPDATE condicional que solo descuenta si stock >= cantidad. Si no afecta a ninguna fila, la API responde 409 y se deshace toda la transacción del pedido.
Por qué
La base de datos decide de forma atómica, sin leer antes ni bloquear a mano. Hay un test que lanza las dos peticiones a la vez.
Código
Test

“dos pedidos a la vez por el último stock: solo uno se confirma”

pedidos.test.ts:114 (se abre en una pestaña nueva)

Sin interbloqueos entre pedidos

Problema
Dos pedidos que comparten productos pueden bloquear filas en orden distinto (A luego B, y B luego A) y esperarse mutuamente para siempre.
Solución
Las líneas de un pedido se procesan siempre ordenadas por productoId.
Por qué
Si todas las transacciones bloquean en el mismo orden, no puede formarse un ciclo de espera. Cuesta una línea de código.
Código

Numeración correlativa sin duplicados

Problema
Pedidos y facturas llevan números correlativos (PED-2026-0001). Dos altas simultáneas pueden leer el mismo último número y generar el mismo siguiente.
Solución
El cálculo del siguiente número se hace bajo un advisory lock de PostgreSQL (pg_advisory_xact_lock) que se libera solo al terminar la transacción.
Por qué
Serializa solo la numeración, no toda la tabla, y no deja bloqueos olvidados.
Código
Test

“pedidos creados a la vez reciben números distintos”

pedidos.test.ts:71 (se abre en una pestaña nueva)

Dinero exacto e histórico inmutable

Problema
Los importes en coma flotante acumulan errores de redondeo, y si una factura lee el precio actual del producto, su total cambia cuando el precio cambia.
Solución
Importes en NUMERIC(10,2) con redondeo explícito del IVA. Cada línea guarda el precio del momento y cada factura sus importes (snapshots).
Por qué
Un documento contable no puede cambiar después de emitido.
Código
Test

“guarda el precio del momento aunque luego cambie el del producto”

pedidos.test.ts:36 (se abre en una pestaña nueva)

Refresh token rotado y renovación compartida

Problema
Un refresh token robado sirve para siempre si no se invalida, y en el frontend varias peticiones que caducan a la vez lanzarían varias renovaciones.
Solución
Cada refresh token sirve una sola vez y se guarda como hash SHA-256. En el frontend, las peticiones que caducan a la vez esperan a una única renovación en curso.
Por qué
Si alguien usa un token robado, el siguiente refresh del usuario legítimo falla y el robo se nota. Y la rotación no rompe la sesión cuando hay peticiones en paralelo.
Código
Test

“rota el refresh token: el usado ya no vale”

auth.test.ts:67 (se abre en una pestaña nueva)

Lo que falta

  • Una factura anulada no se puede volver a emitir: faltan las facturas rectificativas.
  • Los selectores del formulario de pedido cargan hasta 100 elementos; falta búsqueda remota.
  • Sin tests end-to-end del frontend.