Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/160906/Yakultt-App/llms.txt

Use this file to discover all available pages before exploring further.

The Yakult App database (yakult_db) is a normalized MySQL schema of seven tables that together model the full lifecycle of a Yakult distribution operation — from product catalog and client registry, through order creation and delivery, to saved sales reports. The schema is never manually migrated; instead, backend/schema.js exports an ensureSchema() function that runs on every server start. It creates any missing tables, adds any missing columns to existing tables, and creates any missing indexes — all idempotently, so it is safe to run on a brand-new database or one that has been in production for months without risk of data loss.
ensureSchema() uses CREATE TABLE IF NOT EXISTS, ADD COLUMN only after checking information_schema.COLUMNS, and CREATE INDEX only after checking information_schema.STATISTICS. It never drops tables, columns, or rows. You can restart the server at any time without worrying about destructive migrations.
// The server will not listen until the schema is confirmed ready
ensureSchema()
  .then(() => app.listen(PORT, ...))
  .catch((err) => { console.error('Schema init failed:', err); process.exit(1); });

Entity Relationships

clientes ──────────────────┐
                           │ cliente_id (FK)
productos ─────────────────┼──► ordenes ◄── vendedor_id  ──► usuarios
          │                │              └── repartidor_id ──► usuarios
          │ producto_id    └── orden_items ◄── orden_id
          └───────────────────► orden_items
                                
usuarios ──────────────────► notificaciones ◄── orden_id (nullable) ── ordenes
usuarios ──────────────────► reportes_ventas
RelationshipTypeNotes
ordenes.cliente_idclientes.idMany-to-oneA client can have many orders; enforced by FK
ordenes.vendedor_idusuarios.idMany-to-one (nullable)The Promotor who created the order
ordenes.repartidor_idusuarios.idMany-to-one (nullable)The Repartidor assigned to deliver the order
orden_items.orden_idordenes.idMany-to-oneLine items belonging to an order
orden_items.producto_idproductos.idMany-to-oneProduct referenced in a line item
notificaciones.usuario_idusuarios.idMany-to-oneNotifications are scoped to one user
notificaciones.orden_idordenes.idMany-to-one (nullable)Notification may reference an order
reportes_ventas.usuario_idusuarios.idMany-to-oneReport belongs to the user who generated it

Table Reference

productos

Stores the product catalog. Every item that can appear in an order must first exist here.
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
nombreVARCHAR(100) NOT NULLDisplay name of the product
skuVARCHAR(50) NOT NULL UNIQUEStock-keeping unit — must be unique across all products
precioDECIMAL(10,2) NOT NULLUnit sale price
stockINT NOT NULL DEFAULT 0Current units in inventory
categoriaVARCHAR(80) NOT NULL DEFAULT 'General'Product category; used as a report filter
creado_enTIMESTAMP DEFAULT CURRENT_TIMESTAMPRecord creation timestamp
CREATE TABLE IF NOT EXISTS productos (
  id        INT AUTO_INCREMENT PRIMARY KEY,
  nombre    VARCHAR(100)  NOT NULL,
  sku       VARCHAR(50)   NOT NULL UNIQUE,
  precio    DECIMAL(10,2) NOT NULL,
  stock     INT           NOT NULL DEFAULT 0,
  categoria VARCHAR(80)   NOT NULL DEFAULT 'General',
  creado_en TIMESTAMP     DEFAULT CURRENT_TIMESTAMP
);
Constraints: sku is UNIQUE — duplicate SKUs are rejected at the database level.
Index: idx_productos_categoria (categoria) — speeds up category-filtered report queries.

clientes

Stores the client (customer) registry. Clients are referenced by orders.
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
nombreVARCHAR(100) NOT NULLClient full name or business name
telefonoVARCHAR(20)Contact phone number (optional)
direccionVARCHAR(200)Delivery address (optional)
activoTINYINT(1) NOT NULL DEFAULT 1Soft-delete flag; 0 = inactive
creado_enTIMESTAMP DEFAULT CURRENT_TIMESTAMPRecord creation timestamp
CREATE TABLE IF NOT EXISTS clientes (
  id        INT AUTO_INCREMENT PRIMARY KEY,
  nombre    VARCHAR(100) NOT NULL,
  telefono  VARCHAR(20),
  direccion VARCHAR(200),
  activo    TINYINT(1) NOT NULL DEFAULT 1,
  creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Constraints: None beyond the PK. Deactivating a client (activo = 0) hides them from the client picker in the order form without deleting their historical order records.

usuarios

Stores every registered user. Passwords are stored as bcryptjs hashes (cost factor 10).
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
nombreVARCHAR(100) NOT NULLFull display name
correoVARCHAR(120) NOT NULL UNIQUEEmail address — used as login identifier
contrasenaVARCHAR(255) NOT NULLbcrypt hash of the password
rolENUM('Master','Promotor','Repartidor') NOT NULL DEFAULT 'Promotor'Access role
activoTINYINT(1) NOT NULL DEFAULT 1Account enabled/disabled flag
creado_enTIMESTAMP DEFAULT CURRENT_TIMESTAMPRegistration timestamp
CREATE TABLE IF NOT EXISTS usuarios (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  nombre     VARCHAR(100) NOT NULL,
  correo     VARCHAR(120) NOT NULL UNIQUE,
  contrasena VARCHAR(255) NOT NULL,
  rol        ENUM('Master','Promotor','Repartidor') NOT NULL DEFAULT 'Promotor',
  activo     TINYINT(1) NOT NULL DEFAULT 1,
  creado_en  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Constraints: correo is UNIQUE — registering with an already-used email returns a 400 error.
ENUM values: 'Master' | 'Promotor' | 'Repartidor'

ordenes

The central transaction table. Each row represents one sales order from a client.
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
cliente_idINT NOT NULLFK → clientes.id — the purchasing client
vendedor_idINT NULLFK → usuarios.id — Promotor who created the order
repartidor_idINT NULLFK → usuarios.id — Repartidor assigned to deliver
totalDECIMAL(10,2) NOT NULLSum of all line-item totals
estadoENUM('Pendiente','En camino','Entregado') DEFAULT 'Pendiente'Delivery lifecycle state
fechaTIMESTAMP DEFAULT CURRENT_TIMESTAMPOrder creation timestamp
CREATE TABLE IF NOT EXISTS ordenes (
  id            INT AUTO_INCREMENT PRIMARY KEY,
  cliente_id    INT           NOT NULL,
  vendedor_id   INT           NULL,
  repartidor_id INT           NULL,
  total         DECIMAL(10,2) NOT NULL,
  estado        ENUM('Pendiente','En camino','Entregado') DEFAULT 'Pendiente',
  fecha         TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);
ENUM values for estado:
ValueMeaning
PendienteOrder created, not yet dispatched
En caminoAssigned to a repartidor, in transit
EntregadoDelivered and confirmed
Indexes (all created via createIndexIfMissing in ensureSchema):
Index nameColumnsPurpose
idx_ordenes_fechafechaFast single-date and date-range scans
idx_ordenes_cliente_fechacliente_id, fechaPer-client order history with date filter
idx_ordenes_vendedor_fechavendedor_id, fechaPer-promotor sales queries used in reports
idx_ordenes_repartidor_fecharepartidor_id, fechaPer-repartidor delivery history

orden_items

Stores the individual line items (products + quantities) for each order. An order has one or more items.
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
orden_idINT NOT NULLFK → ordenes.id — parent order
producto_idINT NOT NULLFK → productos.id — product sold
cantidadINT NOT NULLQuantity sold
precio_unitDECIMAL(10,2) NOT NULLUnit price at time of sale (snapshot)
CREATE TABLE IF NOT EXISTS orden_items (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  orden_id    INT           NOT NULL,
  producto_id INT           NOT NULL,
  cantidad    INT           NOT NULL,
  precio_unit DECIMAL(10,2) NOT NULL,
  FOREIGN KEY (orden_id)    REFERENCES ordenes(id),
  FOREIGN KEY (producto_id) REFERENCES productos(id)
);
Design note: precio_unit captures the price at the moment of sale, not the current product price. This ensures historical orders remain accurate even after product prices change. Indexes:
Index nameColumnsPurpose
idx_orden_items_orden_productoorden_id, producto_idFast join between ordenes and their items
idx_orden_items_productoproducto_idLookup of all orders containing a given product

notificaciones

Stores in-app notifications delivered to individual users, most commonly triggered when a repartidor is assigned to an order.
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
usuario_idINT NOT NULLFK → usuarios.id — notification recipient
orden_idINT NULLFK → ordenes.id — related order (optional)
tipoVARCHAR(30) NOT NULL DEFAULT 'info'Notification category (e.g., 'info', 'alerta')
tituloVARCHAR(120) NOT NULLShort notification headline
mensajeVARCHAR(255) NOT NULLFull notification body text
leidaTINYINT(1) NOT NULL DEFAULT 0Read/unread flag; 1 = read
creado_enTIMESTAMP DEFAULT CURRENT_TIMESTAMPCreation timestamp
CREATE TABLE IF NOT EXISTS notificaciones (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  usuario_id INT          NOT NULL,
  orden_id   INT          NULL,
  tipo       VARCHAR(30)  NOT NULL DEFAULT 'info',
  titulo     VARCHAR(120) NOT NULL,
  mensaje    VARCHAR(255) NOT NULL,
  leida      TINYINT(1)   NOT NULL DEFAULT 0,
  creado_en  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Index: idx_notif_usuario (usuario_id, leida, creado_en) — the GET /api/notificaciones endpoint filters by usuario_id (sourced from the x-user-id request header) and optionally by unread status, then orders by creado_en DESC. This composite index covers all three operations in a single scan.

reportes_ventas

Stores saved sales report snapshots. When a user runs a report, both the filter parameters (filtros_json) and the computed results (resultado_json) are persisted so the report can be viewed again without re-querying.
ColumnTypeDescription
idINT AUTO_INCREMENTPrimary key
usuario_idINT NOT NULLFK → usuarios.id — user who generated the report
nombreVARCHAR(140) NOT NULLHuman-readable report name
fecha_inicioDATE NOT NULLReport date range start
fecha_finDATE NOT NULLReport date range end
cliente_idINT NULLOptional client filter applied
producto_idINT NULLOptional product filter applied
categoriaVARCHAR(80) NULLOptional category filter applied
vendedor_idINT NULLOptional vendedor filter applied
agrupacionENUM('dia','semana','mes','anio') NOT NULL DEFAULT 'dia'Time-grouping granularity
total_ventasINT NOT NULL DEFAULT 0Total number of orders in the report
total_ingresosDECIMAL(12,2) NOT NULL DEFAULT 0Total revenue across the report period
filtros_jsonLONGTEXT NOT NULLJSON serialization of all applied filters
resultado_jsonLONGTEXT NOT NULLJSON serialization of the computed result rows
creado_enTIMESTAMP DEFAULT CURRENT_TIMESTAMPReport generation timestamp
CREATE TABLE IF NOT EXISTS reportes_ventas (
  id             INT AUTO_INCREMENT PRIMARY KEY,
  usuario_id     INT NOT NULL,
  nombre         VARCHAR(140) NOT NULL,
  fecha_inicio   DATE NOT NULL,
  fecha_fin      DATE NOT NULL,
  cliente_id     INT NULL,
  producto_id    INT NULL,
  categoria      VARCHAR(80) NULL,
  vendedor_id    INT NULL,
  agrupacion     ENUM('dia','semana','mes','anio') NOT NULL DEFAULT 'dia',
  total_ventas   INT NOT NULL DEFAULT 0,
  total_ingresos DECIMAL(12,2) NOT NULL DEFAULT 0,
  filtros_json   LONGTEXT NOT NULL,
  resultado_json LONGTEXT NOT NULL,
  creado_en      TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ENUM values for agrupacion: 'dia' | 'semana' | 'mes' | 'anio' Index: idx_reportes_usuario_fecha (usuario_id, creado_en) — scopes report listings to the requesting user and orders them by most recent.

Index Summary

TableIndex nameColumnsQuery pattern it serves
ordenesidx_ordenes_fechafechaDate-range scans across all orders
ordenesidx_ordenes_cliente_fechacliente_id, fechaPer-client order history
ordenesidx_ordenes_vendedor_fechavendedor_id, fechaPer-promotor sales reports
ordenesidx_ordenes_repartidor_fecharepartidor_id, fechaPer-repartidor delivery history
notificacionesidx_notif_usuariousuario_id, leida, creado_enPer-user notification list, unread filter, sorted by date
orden_itemsidx_orden_items_orden_productoorden_id, producto_idJoin between orders and line items
orden_itemsidx_orden_items_productoproducto_idAll orders containing a specific product
productosidx_productos_categoriacategoriaCategory-filtered product queries and reports
reportes_ventasidx_reportes_usuario_fechausuario_id, creado_enPer-user report list, sorted by creation date
All indexes are created by createIndexIfMissing(), which checks information_schema.STATISTICS before issuing CREATE INDEX, so re-running ensureSchema() never produces a duplicate-index error.

Auto-Migration Pattern

ensureSchema() is structured as a single async function that runs once per server process. A module-level promise cache (schemaReady) ensures it is called only once even if multiple imports request it concurrently:
let schemaReady = null;

function ensureSchema() {
  if (!schemaReady) schemaReady = ensureSchemaInternal();
  return schemaReady;
}

async function ensureSchemaInternal() {
  // 1. CREATE TABLE IF NOT EXISTS for every table
  await db.query(`CREATE TABLE IF NOT EXISTS productos ( ... )`);
  // ... (all 7 tables)

  // 2. ADD missing columns to tables that may pre-date a column
  if (await tableExists('usuarios')) {
    await addColumnIfMissing('usuarios', 'rol',
      "ENUM('Master','Promotor','Repartidor') NOT NULL DEFAULT 'Promotor'");
    await addColumnIfMissing('usuarios', 'activo', 'TINYINT(1) NOT NULL DEFAULT 1');
    // Force the ENUM to include Repartidor even on old tables
    await db.query(
      "ALTER TABLE usuarios MODIFY COLUMN rol " +
      "ENUM('Master','Promotor','Repartidor') NOT NULL DEFAULT 'Promotor'"
    );
  }

  // 3. CREATE indexes if missing
  await createIndexIfMissing('ordenes', 'idx_ordenes_fecha', 'fecha');
  // ... (all 9 indexes)
}
The key helper functions used internally:
HelperChecksAction if missing
tableExists(table)information_schema.TABLESReturns boolean — used before addColumnIfMissing calls
columnExists(table, col)information_schema.COLUMNSReturns boolean
addColumnIfMissing(table, col, def)Uses columnExistsRuns ALTER TABLE … ADD COLUMN
indexExists(table, name)information_schema.STATISTICSReturns boolean
createIndexIfMissing(table, name, cols)Uses indexExistsRuns CREATE INDEX

Build docs developers (and LLMs) love