Arquitectura del Proyecto

Guía visual para entender la estructura: React + Node.js + Sequelize + Docker

1. La Vista General: Los Tres Mundos

🍽️ El Comedor (Frontend)

Ubicación: frontend/

Es lo que ve el cliente. Aquí vive React, los componentes visuales (botones, formularios, menús) y las páginas. Su trabajo es pedirle datos al backend y mostrarlos bonitos.

🍳 La Cocina (Backend)

Ubicación: backend/

Es donde se hace el trabajo real. Aquí vive Node.js con Express. Su trabajo es recibir las peticiones, procesarlas (leer/escribir en BD, hacer cálculos) y devolver una respuesta (JSON).

👨‍🍳 El Jefe de Cocina (Docker)

Archivos: docker-compose.yml / Dockerfile

Se asegura de que la cocina y el comedor funcionen. Le dice a Docker cómo construir y ejecutar tanto el frontend como el backend para que trabajen juntos.

¿Cómo se hablan el Comedor y la Cocina?

El frontend nunca toca la base de datos directamente. Hace una llamada a una API y el backend devuelve la información en formato JSON.

Frontend
vite.config.js (proxy)
api.js
➡️
Petición (API)
ej. /api/mesas
➡️
Backend
Busca en BD y devuelve JSON
Claro. Aquí tienes una explicación detallada de la estructura de tu proyecto, justo como me has pedido: un mapa general que explique el porqué de cada carpeta y qué tipo de archivos encontrarás en cada ruta de forma organizada, creativa y muy fácil de digerir.

🗺️ Mapa de Archivos del Proyecto Mochi Matcha (React + Node.js)

El proyecto está dividido en dos aplicaciones independientes, cada una con su propio Dockerfile y package.json. El archivo central docker-compose.yml las une en una sola red para que trabajen como un equipo.

La Estructura Principal

mochi-matcha/
├── 📁 backend/ ← La Cocina (Node.js + Express)
├── 📁 frontend/ ← El Comedor (React + Vite)
├── 📄 docker-compose.yml ← El Jefe de Cocina (Orquesta los contenedores)
└── 📄 CLAUDE.md ← Las reglas no negociables del proyecto

1. La Cocina: backend/

Esta carpeta contiene TODO el servidor del restaurante. Es una aplicación pura de Node.js que expone las llamadas de API.

backend/
├── 📄 Dockerfile← Instrucciones para construir el contenedor de Node.js
├── 📄 package.json← Dependencias del backend
├── 📁 src/← CÓDIGO FUENTE PRINCIPAL
│ ├── 📄 server.js← Punto de entrada. Arranca Express y la BD.
│ ├── 📄 app.js← Configuración de Express (middlewares, rutas, errores).
│ ├── 📁 config/
│ │ └── 📄 database.js← Conexión a MariaDB/MySQL usando Sequelize.
│ ├── 📁 routes/← CAMAREROS: Reciben peticiones HTTP y las redirigen al controlador.
│ │ ├── 📄 auth.routes.js
│ │ ├── 📄 bienvenida.routes.js
│ │ ├── 📄 mesas.routes.js
│ │ └── ...
│ ├── 📁 controllers/← COCINEROS: Lógica de negocio real. Procesan datos y responden.
│ │ ├── 📄 mesas.controller.js
│ │ ├── 📄 mesero.controller.js
│ │ └── ...
│ ├── 📁 models/← LIBRO DE RECETAS: Definiciones de tablas (Sequelize).
│ │ ├── 📄 Mesa.js
│ │ ├── 📄 Empleado.js
│ │ ├── 📄 index.js← El directorio que conecta todos los modelos entre sí.
│ │ └── ...
│ ├── 📁 middleware/← GUARDIAS: Se ejecutan antes que los controladores.
│ │ ├── 📄 auth.js← Verifica JWT (staff).
│ │ ├── 📄 authorize.js← Verifica roles (mesero, gerente...).
│ │ ├── 📄 mmSession.js← Verifica cookie de sesión de cliente.
│ │ └── ...
│ ├── 📁 services/← PINCHES: Funciones auxiliares específicas.
│ │ └── 📄 kds.js← Filtro para la pantalla de cocina.
│ ├── 📁 utils/← UTILIDADES: Funciones de ayuda generales.
│ │ ├── 📄 promociones.js← Lógica de descuentos.
│ │ ├── 📄 auditoria.js← Registro de acciones importantes.
│ │ └── ...
│ └── 📄 seed.js← Script para llenar la BD con datos de prueba.
├── 📁 public/← ARCHIVOS PÚBLICOS
│ └── 📁 media/← Imágenes de productos servidas por Express.
└── 📁 tests/← Pruebas automatizadas del backend.

¿Por qué esta estructura en el Backend?

Sigue el patrón de diseño Modelo-Vista-Controlador (MVC). De esta manera, cada archivo tiene una única y clara responsabilidad, facilitando su mantenimiento:

  • Si quieres saber qué endpoints existen, vas a la carpeta routes/.
  • Si quieres entender la lógica de un endpoint, vas a la carpeta controllers/.
  • Si quieres ver la estructura de la base de datos, vas a la carpeta models/.

2. El Comedor: frontend/

Esta carpeta contiene la aplicación SPA de React compilada y optimizada con Vite que tus usuarios y tu personal verán en sus dispositivos.

frontend/
├── 📄 Dockerfile.dev← Dockerfile para DESARROLLO (con Vite HMR)
├── 📄 package.json← Dependencias del frontend
├── 📄 vite.config.js← Configuración de Vite (proxy, puerto...)
├── 📄 index.html← Plantilla HTML principal
├── 📁 src/
│ ├── 📄 main.jsx← Punto de entrada de React
│ ├── 📄 App.jsx← Componente raíz con React Router
│ ├── 📁 services/
│ │ └── 📄 api.js← EL TELÉFONO: Configura Axios para llamar al backend.
│ ├── 📁 context/
│ │ └── 📄 AuthContext.jsx← Estado global de autenticación (JWT, roles)
│ ├── 📁 components/← COMPONENTES REUTILIZABLES (piezas de interfaz)
│ │ ├── 📄 ClienteLayout.jsx← Plantilla base para las páginas del cliente (barra inferior)
│ │ ├── 📄 StaffLayout.jsx← Plantilla base para las páginas del staff (sidebar)
│ │ └── ...
│ ├── 📁 pages/← PÁGINAS COMPLETAS (vistas)
│ │ ├── 📁 cliente/← Vistas de la app cliente (móvil)
│ │ │ ├── 📄 Bienvenida.jsx
│ │ │ ├── 📄 MenuCliente.jsx
│ │ │ └── ...
│ │ ├── 📁 mesero/← Vistas del panel de mesero
│ │ │ ├── 📄 MapaMesas.jsx
│ │ │ └── ...
│ │ ├── 📁 cocina/← Vista del KDS
│ │ │ └── 📄 KDS.jsx
│ │ └── 📁 gerente/← Vistas del panel de gerente
│ │ ├── 📄 Dashboard.jsx
│ │ └── ...
│ └── 📁 assets/
│ └── 📁 css/← Estilos CSS (mochi.css, staff.css, cliente.css)
└── 📁 dist/← CÓDIGO COMPILADO para producción (no se toca)

¿Por qué esta estructura en el Frontend?

Sigue los estándares modernos de desarrollo en React: mantenemos **componentes visuales pequeños y modulares** dentro de components/, y las **vistas completas asociadas a rutas** dentro de pages/. Organizar las páginas por roles (cliente, mesero, cocina, gerente) hace que el flujo de trabajo de cada usuario sea modular e independiente.

Con este mapa de archivos, ya tienes los tres pilares del restaurante sumamente claros:

  1. backend/src/routes/ y controllers/: El camarero y el cocinero de la app.
  2. backend/src/models/: El libro de recetas (la base de datos a través de Sequelize).
  3. frontend/src/pages/ y services/: El comedor interactivo (React) y su canal de comunicación directa (api.js).

3. Estructura del Backend (Patrón MVC)

Ubicado en backend/src/. El código está organizado por su responsabilidad.

🚪

server.js / app.js (Puerta de entrada)

Aquí se configura Express, se conecta a la base de datos y se "montan" las rutas. Es el punto de partida del servidor.

🤵

routes/ (Las Rutas - El Camarero)

Su ÚNICO trabajo es recibir la petición (ej. GET /api/mesas) y decidir qué "controller" debe encargarse. Archivos simples y perfectos para empezar a leer.

🧑‍🍳

controllers/ (Los Controladores - El Cocinero)

Aquí está la lógica real. Recibe la petición del "camarero", procesa lo necesario (consulta la BD, valida datos) y devuelve la respuesta. Son el corazón del backend.

📖

models/ (Los Modelos - El Libro de Recetas)

Definen la estructura de datos (tablas). Un modelo como Mesa.js le dice a Sequelize que una mesa tiene un número, capacidad, etc. El controller los usa para no escribir SQL directamente.

👮

middleware/ (Los Middlewares - El Guardia de Seguridad)

Funciones que se ejecutan antes de llegar al controller. Se usan para verificar autenticación (auth.js), permisos (authorize.js) o validar datos (validate.js).

🔪

utils/ (Utilidades - El Pinche)

Funciones de ayuda usadas en varios controladores. Ej: promociones.js puede tener la lógica para calcular descuentos usada tanto en pedidos como en carrito.

Empecemos por el archivo que hace de "pegamento" entre todo: backend/src/app.js. Es el más importante para entender la estructura general. Luego veremos los otros dos.

4. El "Cerebro" del Backend: backend/src/app.js

Este archivo es la cocina principal. Configura todo Express y le dice qué hacer con cada petición. Te lo explico línea por línea.

app.js - Importaciones
const express = require('express');
const cors = require('cors');
const path = require('path');

// Importa todos los archivos de rutas que vimos antes
const authRoutes       = require('./routes/auth.routes');
const bienvenidaRoutes = require('./routes/bienvenida.routes');
// ... etc ...

const app = express(); // Crea la aplicación Express

Aquí no hay magia: importa las herramientas (express, cors, path) y los archivos de rutas que tú me enseñaste (mesas.routes, etc.).

app.js - Configuración Express
// Habilita CORS para que el frontend (en otro puerto) pueda llamar a la API
app.use(cors({ origin: process.env.CORS_ORIGIN }));

// Permite que Express entienda JSON en las peticiones (req.body)
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

Esto es configuración estándar. cors evita errores de seguridad en el navegador.

app.js - Archivos Estáticos
// Sirve las imágenes de productos desde la carpeta /public/media
app.use('/media', express.static(path.join(__dirname, '../public/media')));

Importante: Esta línea hace que las imágenes de tu carpeta public/media/images/ sean accesibles desde el navegador en la ruta /media. Por ejemplo, /media/matchalatte.jpg apuntará a public/media/images/matchalatte.jpg. Así funciona igual que en Django.

app.js - Montaje de Rutas
// Monta las rutas. Cada prefijo se conecta con su archivo de rutas correspondiente
app.use('/api/auth',        authRoutes);      // Login, logout
app.use('/api/bienvenida',  bienvenidaRoutes); // Flujo QR inicial del cliente
app.use('/api/mesas',       mesasRoutes);      // CRUD de mesas y sesiones
app.use('/api/menu',        menuRoutes);       // Productos, categorías, etc.
app.use('/api/pedidos',     pedidosRoutes);    // Crear pedidos, cambiar estado
app.use('/api/cocina',      cocinaRoutes);     // Pantalla KDS
app.use('/api/gerente',     gerenteRoutes);    // CRUDs administrativos, reportes
app.use('/api/cliente',     clienteRoutes);    // Vistas del cliente (carrito, pedidos)
app.use('/api/mesero',      meseroRoutes);     // Panel del mesero (cobro, mapa)
app.use('/api/catalogo',    catalogoRoutes);   // Catálogos (métodos de pago, etc.)

Aquí está la clave. Cuando el frontend llama a GET /api/mesas/estado, Express busca en el archivo mesas.routes.js una ruta que coincida con /estado.

app.js - Manejo de Errores
// Si ninguna ruta coincide, devuelve un error 404 en JSON
app.use((req, res) => {
  res.status(404).json({ error: 'Ruta no encontrada' });
});

// Si algo falla en el código, este middleware captura el error
app.use((err, req, res, next) => {
  console.error(err.message);
  res.status(500).json({ error: 'Error interno del servidor' });
});

module.exports = app;

Los middlewares de error son los "salvavidas". Si un controller falla (por ejemplo, la base de datos no responde), este código evita que el servidor entero se caiga y devuelve un error claro.

5. El "Pinche de Cocina": backend/src/services/kds.js

Te preguntabas qué es la carpeta services. Es una utilidad, no un controller ni un modelo. Este archivo en concreto ayuda al KDS (Pantalla de Cocina) a decidir qué pedidos mostrar.

kds.js - Lógica de Áreas
// 'cocina' ve cocina+ambos; 'bar' ve bar+ambos; sin area → todos.
function areasDe(area) {
  if (area === 'bar')    return new Set(['bar', 'ambos']);
  if (area === 'cocina') return new Set(['cocina', 'ambos']);
  return new Set(['cocina', 'bar', 'ambos']);
}

// Retorna true si el detalle (producto) pertenece al área del KDS.
function detalleEsDeArea(detalle, areasSet) {
  return areasSet.has(detalle.producto?.categoria?.area);
}

module.exports = { areasDe, detalleEsDeArea };

Explicación:

  • Un producto está en una categoría. Esa categoría tiene un area (cocina, bar o ambos).
  • Cuando la cocina (/cocina/kds/?area=cocina) pide los pedidos, solo quiere ver los de cocina y los que son "ambos".
  • Este archivo exporta dos funciones que el controller de cocina usa para filtrar la lista de pedidos antes de enviarla al frontend. Así no ve la cocina los pedidos del bar y viceversa.

Es un ejemplo perfecto de "utilidad": código que se usa en varios sitios, no recibe peticiones HTTP directamente.

6. ¿Y la carpeta utils/?

Los archivos en utils/ son lo mismo que services/: funciones auxiliares. En tu proyecto tienes:

📝

auditoria.js

Probablemente registra acciones importantes (quién pagó, quién canceló un pedido).

🏷️

promociones.js

La lógica de descuentos que se aplica a los pedidos. Se usa tanto cuando el cliente pide desde su app como cuando el mesero hace un pedido asistido.

⚙️

configuracion.js

Lee la configuración general del restaurante (horarios, umbrales del semáforo KDS).

🖼️

imagenEfectiva.js

Posamente maneja la lógica de elegir la mejor imagen para un producto.

Excelente, hemos llegado a una parte crucial: vamos a ver cómo el "camarero" (rutas) le pasa la orden al "cocinero" (controlador), y cómo el "cliente" (frontend) se comunica con todo el sistema. También veremos a los "guardias de seguridad" (middlewares). Todo encaja perfectamente.

7. Mapa de Flujo del Comedor al Corazón del Backend

Esto es lo que cubriremos en este bloque interactivo:

  1. 1 mesas.controller.js: El cocinero que recibe la orden y cocina los datos de las mesas.
  2. 2 api.js: El teléfono directo entre el comedor (React) y la cocina (Express).
  3. 3 ClienteLayout.jsx: Un componente de React que crea la estructura visual para la app del cliente.
  4. 4 authorize.js: El guardia que pide la identificación del personal.
  5. 5 mmSession.js: El portero que reconoce a los clientes.

1. El Cocinero de Mesas: mesas.controller.js

Este archivo es un gran ejemplo de un controlador. Contiene la lógica real para las operaciones con mesas que vimos en las rutas.

exports.listar - Obtener todas las mesas para el mapa

mesas.controller.js
exports.listar = async (req, res, next) => {
  try {
    // 1. Va a la base de datos y pide TODAS las mesas (findAll).
    //    Pero no solo las mesas: con 'include' le pide que también traiga
    //    la información relacionada: ubicación, mesero asignado, alertas activas
    //    y las sesiones activas con sus pedidos "listos".
    const mesas = await Mesa.findAll({
      include: [
        { model: UbicacionMesa, as: 'ubicacion' },
        { model: Empleado, as: 'mesero', attributes: ['id_empleado', 'nombre'] },
        { model: AlertaMesero, as: 'alertas', where: { atendida: false }, required: false },
        {
          model: SesionCliente, as: 'sesiones',
          where: { estado: 'activa' }, required: false,
          include: [{
            model: Pedido, as: 'pedidos',
            where: { estado: 'listo' }, required: false,
          }],
        },
      ],
      order: [['numero_mesa', 'ASC']], // Ordenadas por número
    });

    // 2. Consulta aparte: busca todas las solicitudes de pago que están "pendientes"
    //    para saber qué mesas están esperando cobro.
    const estadoPendiente = await EstadoSolicitud.findOne({ where: { descripcion: 'pendiente' } });
    const solicitudesPendientes = estadoPendiente
      ? await SolicitudPago.findAll({ where: { id_estado_solicitud: estadoPendiente.id } })
      : [];
    const mesasConSolicitud = new Set(solicitudesPendientes.map(s => s.id_mesa));

    // 3. Procesa los datos para el frontend.
    //    Toma los datos crudos y los convierte en un objeto más simple.
    const result = mesas.map(m => {
      const sesiones = m.sesiones || [];
      const alertas = m.alertas || [];
      const pedidosListos = sesiones.reduce((acc, s) => acc + (s.pedidos || []).length, 0);
      const tieneSolicitud = mesasConSolicitud.has(m.id);

      // Lógica para decidir el color y etiqueta de la mesa en el mapa
      let estado_visual;
      if (m.estado === 'libre') estado_visual = 'libre';
      else if (alertas.length > 0) estado_visual = 'alerta';
      else if (tieneSolicitud) estado_visual = 'cobrando';
      else if (pedidosListos > 0) estado_visual = 'listo';
      else estado_visual = 'ocupada';

      return {
        id: m.id,
        numero_mesa: m.numero_mesa,
        estado: m.estado,
        estado_visual,
        pin_actual: m.pin_actual,
        capacidad: m.capacidad,
        ubicacion: m.ubicacion,
        alertas,
        pedidosListos,
        nota_cierre: m.nota_cierre || '',
      };
    });

    // 4. Envía la respuesta al "camarero" (ruta).
    res.json(result);
  } catch (err) { next(err); } // Si algo falla, pasa el error al middleware de errores
};

Explicación sencilla: Esta función es la que se ejecuta cuando el mesero ve el mapa de mesas. Recopila información de muchas tablas a la vez, la procesa y devuelve un JSON limpio listo para pintar en la pantalla.

2. El Teléfono Directo: api.js

Este archivo configura Axios, una librería para hacer llamadas HTTP desde el frontend de forma sencilla.

api.js
import axios from 'ajax';

const api = axios.create({ baseURL: '/api' });

// Interceptor de REQUEST: Se ejecuta ANTES de cada llamada.
// Inyecta automáticamente el token JWT del staff en los headers.
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Interceptor de RESPONSE: Se ejecuta al recibir la respuesta.
// Si el servidor dice 401 (no autorizado), borra la sesión local y redirige al login.
api.interceptors.response.use(
  (res) => res,
  (err) => {
    if (err.response?.status === 401) {
      localStorage.removeItem('token');
      localStorage.removeItem('empleado');
      window.location.href = '/login';
    }
    return Promise.reject(err);
  }
);

export default api;

Explicación sencilla: Imagina que api es un mensajero. Antes de salir a hacer una entrega (request), siempre mete en el bolsillo la tarjeta de identificación (token). Si cuando vuelve le dicen que la tarjeta no es válida (error 401), borra todo y te manda a la puerta de entrada (/login).

3. La Estructura de la App Cliente: ClienteLayout.jsx

Este es un componente de React que define la estructura común de todas las pantallas del cliente (menú, carrito, pedidos).

ClienteLayout.jsx
export default function ClienteLayout({ children, idSesion, carritoCount = 0 }) {
  // ... hooks de navegación ...

  // Función para llamar al mesero (botón de ayuda)
  const solicitarAyuda = async (e) => {
    e.preventDefault();
    const token_sesion = sessionStorage.getItem('token_sesion');
    if (!token_sesion) return;
    try {
      // Llama al endpoint que crea una alerta en la mesa
      await api.post('/cliente/alerta', { token_sesion, tipo: 'ayuda', mensaje: '' });
    } catch { /* ignore */ }
  };

  return (
    
{/* Contenedor principal con estilo de teléfono */}
{children} {/* Aquí se renderiza el contenido de cada página */}
{/* Barra de navegación inferior */} {idSesion && ( )}
); }

Explicación sencilla: Este componente es como el "molde" de una pantalla de cliente. Tiene un área de contenido (donde va cada página) y una barra de navegación inferior fija. El botón de "Ayuda" llama directamente al backend usando api.js.

4. El Guardia del Personal: authorize.js

Este middleware verifica que el usuario que hace la petición tenga el rol adecuado.

authorize.js
const authorize = (...roles) => (req, res, next) => {
  // 'req.user' fue añadido por el middleware 'auth.js' (que se ejecutó antes)
  if (!req.user) return res.status(401).json({ error: 'No autenticado' });
  // Comprueba si el rol del usuario está en la lista de roles permitidos
  if (!roles.includes(req.user.rol)) return res.status(403).json({ error: 'Acceso denegado' });
  next(); // Todo bien, pasa al siguiente paso (el controlador)
};

// Atajos para los roles más comunes
const requireMesero  = authorize('mesero', 'gerente', 'admin');
const requireCocina  = authorize('cocina', 'gerente', 'admin');
const requireGerente = authorize('gerente', 'admin');

module.exports = Object.assign(authorize, { requireMesero, requireCocina, requireGerente });

Explicación sencilla: Este es el guardia de la puerta de la cocina. Cuando llega una petición de un mesero, revisa su gafete (req.user.rol) y solo lo deja pasar si su rol está en la lista de invitados. Por ejemplo, requireMesero deja pasar a meseros, gerentes y admins.

5. El Portero de Clientes: mmSession.js

Este middleware autentica a los clientes (no al staff). Los clientes no usan JWT, sino una cookie especial llamada mm_session.

mmSession.js
const cookie = require('cookie');
const { SesionCliente } = require('../models');

module.exports = async (req, res, next) => {
  try {
    // Lee la cookie 'mm_session' del navegador
    const cookies = cookie.parse(req.headers.cookie || '');
    const token = cookies.mm_session;
    if (!token) return res.status(401).json({ ok: false, error: 'Sesión requerida' });

    // Busca la sesión del cliente en la base de datos
    const sesion = await SesionCliente.findOne({ where: { token_cookie: token } });
    if (!sesion) return res.status(401).json({ ok: false, error: 'Sesión inválida' });

    // Si todo es correcto, deja la sesión disponible para el controlador
    req.sesionCliente = sesion;
    req.mmToken = token;
    next();
  } catch (err) { next(err); }
};

Explicación sencilla: Cuando un cliente escanea el QR, se le da una "pulsera" con un código (la cookie mm_session). Este middleware es el portero que lee esa pulsera, verifica que sea válida y, si es correcta, deja pasar al cliente al área de pedidos.

Te explico los dos archivos. Son la definición de las tablas mesas y empleados en tu base de datos, usando Sequelize. No son rutas ni controladores, sino el "molde" que Sequelize usa para crear las tablas y leer/escribir datos.

8.1. El Modelo Mesa.js – La Tabla de Mesas

Este archivo define exactamente qué columnas tiene la tabla mesas en tu base de datos.

Mesa.js
const { DataTypes } = require('sequelize');
const { v4: uuidv4 } = require('uuid');

module.exports = (sequelize) => {
  return sequelize.define('Mesa', {
    id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    numero_mesa: { type: DataTypes.INTEGER, allowNull: false, unique: true },
    capacidad:   { type: DataTypes.INTEGER, allowNull: false },
    codigo_qr:   { type: DataTypes.STRING(255), unique: true },
    pin_actual:  { type: DataTypes.STRING(60), allowNull: true },
    estado: {
      type: DataTypes.ENUM('libre', 'ocupada'),
      defaultValue: 'libre',
    },
    nota_cierre: { type: DataTypes.STRING(255), defaultValue: '' },
  }, {
    tableName: 'mesas',
    hooks: {
      beforeCreate(m) {
        if (!m.codigo_qr) {
          m.codigo_qr = `mesa-${m.numero_mesa}-${uuidv4().replace(/-/g, '').slice(0, 8)}`;
        }
      },
    },
  });
};

Explicación línea por línea:

  1. sequelize.define('Mesa', { ... }) – Le dice a Sequelize: "Crea una tabla llamada mesas con estas columnas". El primer argumento ('Mesa') es el nombre interno del modelo, y con tableName: 'mesas' le decimos que la tabla en la base de datos se llama mesas.
  2. Columnas (los campos de la tabla):
    • id: Número único que identifica cada mesa. autoIncrement: true significa que la base de datos genera automáticamente 1, 2, 3...
    • numero_mesa: El número visible (Mesa 1, Mesa 2). unique: true impide que haya dos mesas con el mismo número.
    • capacidad: Cuántas personas caben.
    • codigo_qr: Un código único para el QR de la mesa. No se pide al crear la mesa, se genera solo (lo verás abajo en hooks).
    • pin_actual: El PIN de 4 dígitos que los clientes usan para unirse a la mesa. Puede ser nulo si la mesa está libre.
    • estado: Puede ser 'libre' o 'ocupada'. defaultValue: 'libre' significa que una mesa nueva empieza libre.
    • nota_cierre: Un campo de texto para poner una nota cuando la mesa se cierra manualmente.
  3. hooks – Son funciones que se ejecutan automáticamente en ciertos momentos:
    • beforeCreate(m): Justo antes de que una mesa se guarde por primera vez. Si no se le ha puesto un codigo_qr, aquí se genera uno automáticamente usando el número de mesa y un código aleatorio. Esto asegura que cada mesa tenga un QR único.

¿Cómo se conecta esto con el controlador listar que vimos antes?

Cuando haces Mesa.findAll({ include: [...] }), Sequelize lee este archivo para saber qué columnas buscar en la tabla mesas y cómo se relacionan con otras tablas (como SesionCliente o UbicacionMesa). Las relaciones no están definidas en este archivo, sino en un archivo central models/index.js, pero eso lo veremos después.

8.2. Bonus: El Modelo Empleado.js – La Tabla de Empleados

Este modelo es muy similar, pero incluye un detalle importante: seguridad de contraseñas.

Empleado.js
const { DataTypes } = require('sequelize');
const bcrypt = require('bcryptjs');

module.exports = (sequelize) => {
  const Empleado = sequelize.define('Empleado', {
    id_empleado: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    nombre:  { type: DataTypes.STRING(100), allowNull: false },
    usuario: { type: DataTypes.STRING(50),  allowNull: false, unique: true },
    password:{ type: DataTypes.STRING(255), allowNull: false },
    rol: {
      type: DataTypes.ENUM('mesero', 'cocina', 'gerente', 'admin'),
      defaultValue: 'mesero',
    },
    activo:    { type: DataTypes.BOOLEAN, defaultValue: true },
    is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
    is_staff:  { type: DataTypes.BOOLEAN, defaultValue: false },
  }, {
    tableName: 'empleados',
    hooks: {
      beforeSave(e) {
        if (e.nombre) e.nombre = e.nombre.toUpperCase();
      },
      async beforeCreate(e) {
        e.password = await bcrypt.hash(e.password, 10);
      },
    },
  });

  Empleado.prototype.verificarPassword = function (plain) {
    return bcrypt.compare(plain, this.password);
  };

  return Empleado;
};

Explicación adicional sobre Empleado.js:

  1. hooks de Empleado:
    • beforeSave(e): Antes de guardar cualquier empleado, su nombre se convierte automáticamente a MAYÚSCULAS. Así siempre se guarda igual, sin importar cómo lo escriba el gerente.
    • beforeCreate(e): Antes de crear un empleado nuevo, su contraseña se encripta con bcrypt. Así nunca se guarda la contraseña real, solo una versión cifrada por motivos de seguridad informática.
  2. prototype.verificarPassword: Es una función que se añade al prototipo del modelo Empleado. Cuando alguien intenta iniciar sesión, el controlador llama a esta función para comparar la contraseña que escribe el usuario (en texto plano) con la versión cifrada guardada en la base de datos usando bcrypt.compare.

8.3. El Directorio de Modelos: backend/src/models/index.js

Este archivo es el "directorio telefónico" de todos tus modelos. Su función principal es doble:

  1. Inicializar todos los modelos: Carga cada archivo (como Mesa.js, Empleado.js) y los activa con la conexión a la base de datos.
  2. Definir las relaciones entre ellos: Le dice a Sequelize cómo se conectan las tablas entre sí (por ejemplo, que una Mesa tiene muchas SesionCliente).

Es la pieza central que permite que los include que viste en los controladores (mesas.controller.js) funcionen. Sin este archivo, no podrías hacer consultas que unan varias tablas.

1. La Carga de Modelos (El Inicio del Archivo)

models/index.js - Carga
const sequelize = require('../config/database');

// Catálogos
const ModalidadIngreso  = require('./catalogs/ModalidadIngreso')(sequelize);
const MetodoPago        = require('./catalogs/MetodoPago')(sequelize);
// ... etc.

// Modelos principales
const Empleado           = require('./Empleado')(sequelize);
const Mesa               = require('./Mesa')(sequelize);
// ... etc.
  • require('../config/database'): Obtiene la conexión a la base de datos que configuramos en config/database.js.
  • require('./Mesa')(sequelize): Aquí ocurre la magia. Cada modelo es una función que recibe la conexión sequelize y devuelve la definición de la tabla. Es como decirle: "Mesa, aquí tienes la conexión a la base de datos, defínete".

Al hacer esto, todos los modelos quedan listos para usarse, y Sequelize ya sabe que existen tablas como mesas, empleados, etc.

2. Las Asociaciones (El Corazón del Archivo)

Aquí es donde se le dice a Sequelize cómo se relacionan las tablas. Te explico los tipos principales con ejemplos concretos de tu proyecto.

Relación Uno a Muchos: Mesa y SesionCliente

models/index.js - Relación Uno a Muchos
// Una Mesa tiene MUCHAS Sesiones de Cliente
Mesa.hasMany(SesionCliente, { foreignKey: 'id_mesa', onDelete: 'RESTRICT', as: 'sesiones' });
// Una Sesión de Cliente pertenece a UNA Mesa
SesionCliente.belongsTo(Mesa, { foreignKey: 'id_mesa', as: 'mesa' });

Explicación:

  • Mesa.hasMany(SesionCliente, ...): Significa que una mesa (ej. la Mesa 1) puede tener muchas sesiones de cliente. En la base de datos, la tabla sesiones_cliente tiene una columna id_mesa que apunta a la mesa correspondiente.
  • SesionCliente.belongsTo(Mesa, ...): Es el lado inverso. Una sesión de cliente siempre pertenece a una mesa. El foreignKey: 'id_mesa' le dice a Sequelize qué columna usar para la conexión.
  • as: 'sesiones': Este es el alias. Es el nombre que usarás en los controladores para incluir los datos relacionados. Si miras el controlador mesas.controller.js, verás include: [{ model: SesionCliente, as: 'sesiones' }]. Ese as: 'sesiones' coincide con el definido aquí.
  • onDelete: 'RESTRICT': Esta es una regla de integridad. Significa que no se puede borrar una mesa si tiene sesiones activas. Primero debes cerrar las sesiones. Así se evitan errores graves.

Relación Uno a Muchos con SET NULL: Empleado y Mesa

models/index.js - Relación con SET NULL
Empleado.hasMany(Mesa, { foreignKey: 'id_mesero_asignado', onDelete: 'SET NULL' });
Mesa.belongsTo(Empleado, { foreignKey: 'id_mesero_asignado', as: 'mesero' });

Explicación:

  • Un empleado (mesero) puede estar asignado a muchas mesas. Si el empleado es despedido y se borra, no queremos borrar las mesas. onDelete: 'SET NULL' simplemente deja el campo id_mesero_asignado como NULL en la mesa, indicando que ya no tiene mesero asignado.
  • El alias as: 'mesero' permite que en el controlador puedas hacer include: { model: Empleado, as: 'mesero' } para obtener los datos del mesero asignado a la mesa.

Relación Muchos a Muchos: Producto y GrupoModificador

models/index.js - Relación Muchos a Muchos
Producto.belongsToMany(GrupoModificador, {
  through: 'producto_grupos',
  foreignKey: 'id_producto',
  otherKey: 'id_grupo',
  as: 'grupos_modificadores',
});
GrupoModificador.belongsToMany(Producto, {
  through: 'producto_grupos',
  foreignKey: 'id_grupo',
  otherKey: 'id_producto',
  as: 'productos',
});

Explicación:

  • Un producto puede estar en varios grupos de modificadores (ej. "Matcha Latte" está en "Tamaño" y "Tipo de leche"). Un grupo de modificadores puede aplicarse a varios productos.
  • through: 'producto_grupos' indica que esta relación necesita una tabla intermedia en la base de datos (que Sequelize crea automáticamente) para almacenar las conexiones.
  • Los controladores pueden usar los alias as: 'grupos_modificadores' o as: 'productos' para incluir estos datos.

3. ¿Cómo se conecta todo esto?

Ahora ya puedes ver el círculo completo:

  1. models/index.js define que Mesa tiene sesiones (alias).
  2. controllers/mesas.controller.js usa ese alias:
    const mesas = await Mesa.findAll({
      include: [{ model: SesionCliente, as: 'sesiones' }]
    });
  3. Sequelize lee la asociación en index.js, sabe que sesiones usa la columna id_mesa, y arma una consulta SQL que une las dos tablas.
  4. El controlador obtiene las mesas con sus sesiones incluidas y las envía al frontend.
Te voy a guiar por el flujo completo de "Un cliente pide la cuenta", integrando todas las piezas que hemos visto. Después te diré qué seguiría.

9. Flujo Completo: Un Cliente Pide la Cuenta

Vamos a seguir el camino exacto, desde que el cliente toca el botón en su celular hasta que el mesero ve la notificación.

Paso 1: El Cliente Toca "Pedir la Cuenta"

Todo empieza en el frontend, en la página de Pedidos del cliente.

Archivo: frontend/src/pages/cliente/Pedidos.jsx

Pedidos.jsx
const pedirCuenta = async () => {
  const res = await api.post('/cliente/cuenta', {
    tipo: 'individual', // o 'grupal'
  });
  // Muestra un mensaje de éxito
};

Explicación:

  • api.post es el teléfono que configuramos en services/api.js. Llama al backend con el prefijo /api automáticamente.
  • Se envía al backend: POST /api/cliente/cuenta.
  • El token de sesión del cliente viaja de manera segura en la cookie mm_session.

Paso 2: El Guardia Reconoce al Cliente

Antes de que el controlador haga algo, el middleware mmSession.js intercepta la petición.

Archivo: backend/src/middleware/mmSession.js

mmSession.js
const cookies = cookie.parse(req.headers.cookie || '');
const token = cookies.mm_session;
const sesion = await SesionCliente.findOne({ where: { token_cookie: token } });
req.sesionCliente = sesion; // ← El controlador usará esto

Explicación:

  • El middleware lee la cookie mm_session enviada por el navegador.
  • Busca la sesión del cliente en la base de datos de Sequelize.
  • Si es válida, inyecta la información en la petición (req.sesionCliente) dejándola disponible para el controlador.

Paso 3: La Ruta Recibe la Petición

Archivo: backend/src/routes/cliente.routes.js

cliente.routes.js
router.post('/cuenta', mmSession, ctrl.pedirCuenta);

Explicación:

  • El "camarero" (ruta) recibe la orden POST /api/cliente/cuenta.
  • Ejecuta primero mmSession para autenticar y validar la pulsera de identificación del cliente.
  • Luego pasa de forma segura la petición al controlador de negocio ctrl.pedirCuenta.

Paso 4: El Controlador Procesa la Lógica

Archivo: backend/src/controllers/cliente.controller.js

cliente.controller.js
exports.pedirCuenta = async (req, res, next) => {
  try {
    const sesion = req.sesionCliente; // Gracias al middleware
    const tipo = req.body.tipo; // 'individual' o 'grupal'

    // 1. Buscar el estado "pendiente" en el catálogo
    const estadoPendiente = await EstadoSolicitud.findOne({
      where: { descripcion: 'pendiente' }
    });

    // 2. Calcular el total según el tipo
    let total = 0;
    if (tipo === 'individual') {
      const pedidos = await Pedido.findAll({
        where: { id_sesion: sesion.id, estado: { [Op.ne]: 'cancelado' } },
        include: [{ model: DetallePedido, as: 'detalles' }]
      });
      total = pedidos.reduce((acc, p) =>
        acc + p.detalles.reduce((s, d) => s + parseFloat(d.subtotal_calculado), 0), 0
      );
    } else {
      // grupal: sumar todas las sesiones activas de la mesa
      // ...
    }

    // 3. Crear la solicitud de pago
    await SolicitudPago.create({
      id_mesa: sesion.id_mesa,
      id_sesion: tipo === 'individual' ? sesion.id : null,
      tipo,
      total_mesa: total,
      id_estado_solicitud: estadoPendiente.id,
    });

    res.json({ ok: true, mensaje: 'Solicitud enviada' });
  } catch (err) { next(err); }
};

Explicación:

  • Usa los datos de req.sesionCliente previamente resueltos por el middleware de sesión.
  • Consulta múltiples modelos de base de datos interconectados: EstadoSolicitud, Pedido y DetallePedido.
  • Crea un registro de solicitud de pago en la tabla SolicitudPago para la base de datos.
  • Responde de vuelta al frontend de React indicando éxito.

Paso 5: El Mesero Ve la Solicitud

El panel del mesero realiza un sondeo constante (*polling*) de estado cada 3 segundos.

Archivo: frontend/src/pages/mesero/MeseroPanel.jsx

MeseroPanel.jsx
const cargarSolicitudes = async () => {
  const res = await api.get('/mesero/mapa/estado');
  // res.data contiene las mesas con sus solicitudes de cobro vigentes
};

Y el controlador encargado de resolver esta actualización en tiempo real:

Archivo: backend/src/controllers/mesero.controller.js

mesero.controller.js
exports.mesasEstado = async (req, res) => {
  const mesas = await Mesa.findAll({
    include: [
      { model: SesionCliente, as: 'sesiones', where: { estado: 'activa' } },
      { model: SolicitudPago, as: 'solicitudes', where: { id_estado_solicitud: estadoPendiente.id } }
    ]
  });
  res.json(mesas);
};
Este es el flujo más crítico del sistema. Aquí se junta todo: seguridad, integridad de datos y la experiencia del mesero. Vamos a desglosarlo por completo.

💳 Flujo Completo: El Mesero Cobra una Cuenta

Seguiremos el camino desde que el mesero selecciona el método de pago hasta que la mesa queda lista para cerrar.

Paso 1: El Mesero Configura el Pago

Todo empieza en la página de cobro del mesero.

Archivo: frontend/src/pages/mesero/Pago.jsx

El componente carga los datos de la mesa y sus sesiones, y muestra un formulario completo donde el mesero puede:

Pago.jsx
const handleSubmit = async (e) => {
  e.preventDefault();
  // ... validaciones de montos mínimos ...

  const { data } = await api.post('/mesero/pago', {
    mesa_id: parseInt(mesaId, 10),
    sesion_id: tipoCobro === 'individual' ? parseInt(sesionSel, 10) : null,
    metodo_pago_id: metodoSel?.id,
    propina: propinaMonto.toFixed(2),
    monto_recibido: isEfectivo ? montoRecibido : undefined,
    monto_efectivo: isMixto ? montoEfectivo : undefined,
    monto_tarjeta: isMixto ? montoTarjeta : undefined,
  });
  // Redirige al ticket
  navigate(`/mesero/ticket?mesa_id=${mesaId}&sol_id=${data.sol_id || ''}`);
};

Explicación: El frontend solo envía los datos que el mesero ingresó. El backend nunca confía en estos montos para el cálculo final.

Paso 2: El Guardia Verifica al Mesero

Antes de que el controlador haga nada, los middlewares auth.js y authorize.js verifican que la petición tenga un JWT válido y que el usuario tenga rol mesero, gerente o admin.

Si el token no es válido o el rol no tiene permisos, la petición se rechaza inmediatamente con un código de estado HTTP 401 o 403.

Paso 3: La Ruta Recibe la Petición

Archivo: backend/src/routes/mesero.routes.js

mesero.routes.js
router.post('/pago',
  validate({ body: { mesa_id: 'required|int:1,' } }),
  authorize('mesero', 'gerente', 'admin'), ctrl.procesarPago);

El "camarero" (ruta) valida que mesa_id esté presente en el cuerpo de la petición y sea un entero válido, luego pasa al controlador procesarPago.

Paso 4: El Controlador Procesa el Pago (El Corazón del Flujo)

Archivo: backend/src/controllers/mesero.controller.js → función procesarPago

Este es el punto más importante de todo el sistema. Lo dividiremos en tres fases clave:

Fase A: Validaciones Iniciales (sin tocar la base de datos)

mesero.controller.js - Fase A
// 1. Verificar que el método de pago existe
const metodo = await MetodoPago.findByPk(metodo_pago_id);

// 2. Verificar que la mesa existe
const mesa = await Mesa.findByPk(mesa_id);

// 3. Pre-validar que la sesión/mesa no esté ya pagada
if (sesion_id) {
  const sPre = await SesionCliente.findOne({ where: { id: sesion_id, id_mesa: mesa.id } });
  if (sPre.estado !== 'activa') {
    return res.status(409).json({ error: 'La cuenta ya fue saldada.' });
  }
} else {
  const algunaActiva = await SesionCliente.findOne({ where: { id_mesa: mesa.id, estado: 'activa' } });
  if (!algunaActiva) {
    return res.status(409).json({ error: 'La cuenta de esta mesa ya fue saldada.' });
  }
}

Explicación: Es una primera comprobación rápida para rechazar cobros duplicados antes de hacer bloqueos en las transacciones.

Fase B: La Transacción Atómica con Bloqueo

Aquí empieza la magia. Todo lo que sigue está dentro de una transacción gestionada por Sequelize. Esto garantiza que o todo se ejecuta con éxito, o nada se guarda (Rollback automático en caso de fallo).

mesero.controller.js - Transacción Atómica
await sequelize.transaction(async (t) => {
  // 1. Bloquear las sesiones para evitar condiciones de carrera (Race Conditions)
  const sLock = await SesionCliente.findOne({
    where: { id: sesion_id, id_mesa: mesa.id },
    lock: t.LOCK.UPDATE, // ← Bloquea la fila hasta que termine la transacción
    transaction: t,
  });

  // 2. Re-validar el estado bajo el lock de seguridad
  if (!sLock || sLock.estado !== 'activa') {
    bizError = { status: 409, body: { error: 'Esta cuenta ya fue saldada por otro usuario.' } };
    return; // Sale de la transacción
  }

  // 3. Calcular el total REAL consultando la base de datos directamente
  total = await _sumarConsumo({ idSesion: sLock.id }, t);

  // 4. Validar montos de entrada según el método de pago elegido
  if (detalle_pago === 'EFECTIVO') {
    if (montoRecibido < totalConPropina) {
      bizError = { status: 400, body: { error: 'Monto insuficiente.' } };
      return;
    }
    cambio = montoRecibido - totalConPropina;
  } else if (detalle_pago === 'MIXTO') {
    // Validar que efectivo + tarjeta >= total + propina
  }

  // 5. Buscar o crear la SolicitudPago correspondiente
  solicitud = await SolicitudPago.findOne({
    where: { id_sesion: sesion_id, id_estado_solicitud: estadoPendiente.id },
    order: [['fecha_hora', 'ASC']],
    transaction: t,
  });
  if (!solicitud) {
    solicitud = await SolicitudPago.create({...}, { transaction: t });
  }

  // 6. Actualizar la SolicitudPago con los detalles consolidados del cobro
  await solicitud.update({
    id_estado_solicitud: estadoProcesada.id,
    id_metodo_pago: metodo.id,
    propina_sugerida: propinaNum,
    detalle_pago,
    monto_recibido: montoRecibido,
    cambio,
  }, { transaction: t });

  // 7. Marcar las sesiones correspondientes como "pagada"
  for (const s of sesionesLocked) {
    await s.update({ estado: 'pagada' }, { transaction: t });
  }

  // 8. Relación M2M: Registrar qué sesiones cubrió este pago
  await solicitud.setSesiones_cubiertas(
    sesionesLocked.map((s) => s.id),
    { transaction: t }
  );

  // 9. Cerrar otras solicitudes de pago pendientes de la mesa
  await SolicitudPago.update(
    { id_estado_solicitud: estadoProcesada.id },
    { where: { id_mesa: mesa.id, id_estado_solicitud: estadoPendiente.id, id: { [Op.ne]: solicitud.id } }, transaction: t }
  );

  // 10. Post-pago: actualizar la mesa en base de datos sin liberarla
  await _postPagoMesa(mesa, req.user?.id, t);
});

Detalles clave de la Transacción:

  • lock: t.LOCK.UPDATE: Si dos meseros intentan cobrar la misma mesa al mismo tiempo, uno tendrá que esperar a que el otro finalice la transacción, evitando cobros dobles en paralelo.
  • _sumarConsumo: Consulta todos los pedidos no cancelados directamente de la base de datos. No se confía en el monto enviado por el frontend para evitar alteraciones maliciosas del cliente.
  • _postPagoMesa: Esta función auxiliar actualiza la nota de cierre de la mesa ("Cuenta saldada — lista para cerrar" si ya no quedan consumiciones, o el detalle con el saldo restante si quedan sesiones activas).

Fase C: Auditoría (fuera de la transacción)

mesero.controller.js - Auditoría
await Auditoria.create({
  accion: 'Pago procesado',
  detalle: `Mesa ${mesa.numero_mesa} | Método: ${metodo.descripcion} | Total: $${totalFinal.toFixed(2)} | Propina: $${propinaNum.toFixed(2)}`,
  id_empleado: req.user?.id || null,
  id_mesa: mesa.id,
});

La auditoría se crea después de la transacción para no ralentizar la BD principal de cobros. Si por algún motivo la auditoría fallara, el pago se mantiene seguro pero queda el registro del intento en los registros del servidor.

Paso 5: El Modelo SolicitudPago

Archivo: backend/src/models/SolicitudPago.js

Este modelo define la tabla solicitudes_pago en Sequelize con el desglose de montos y variables de cambio:

Columna Descripción
total_individual Total del consumo si es cobro individual (un único cliente).
total_mesa Total acumulado de la mesa entera si es un cobro grupal.
propina_sugerida Monto total de propina agregada por el mesero.
monto_efectivo / monto_tarjeta Desglose de montos específicos en caso de utilizar pago mixto.
monto_recibido Monto exacto en efectivo recibido para realizar el cobro.
cambio El cambio calculado y devuelto al cliente.
detalle_pago Mapeo de texto del tipo de pago (EFECTIVO, TARJETA, MIXTO, PAYPAL).

Paso 6: El Mesero Ve el Ticket

Tras completar de forma segura la transacción, el backend responde al frontend:

Respuesta JSON
res.json({
  ok: true,
  solicitud_id: solicitudId,
  ticket_url: `/mesero/ticket/${solicitudId}/`,
});

El frontend de React redirige automáticamente al mesero a la vista del Ticket, permitiéndole revisar el desglose total, verificar el cambio y opcionalmente imprimir o descargar el archivo PDF.

📊 Resumen Visual del Flujo de Cobro

  1. 1
    Mesero en Pago.jsx: Configura montos, método y envía los datos al backend.
    POST /api/mesero/pago
  2. 2
    Middlewares (auth.js + authorize.js): Verifican la identidad del mesero o gerente mediante su JWT y los roles correspondientes.
  3. 3
    Controlador (procesarPago):
    • Pre-validación: Comprueba el estado básico de la mesa/sesión.
    • Transacción SQL: Bloquea las filas con LOCK.UPDATE, recalcula totales del consumo directamente en BD, actualiza la SolicitudPago, asocia las sesiones liquidadas y procesa las notas de cierre con _postPagoMesa.
  4. 4
    Post-procesamiento y Desvío: Registra la operación en Auditoría (fuera de la transacción) y redirige al mesero al Ticket en el frontend para concluir el proceso.

¡Con esto garantizamos la integridad del dinero y del sistema frente a cualquier error o duplicidad de cobros!