Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Carlos-Gnd/FERRED-Inventario-y-Ventas/llms.txt

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

El módulo de Punto de Venta (POS) es la interfaz principal de Ferred para registrar ventas en mostrador. Lo usan cajeros y administradores para buscar productos por código de barras o nombre, armar el carrito, confirmar la venta y generar automáticamente el Documento Tributario Electrónico (DTE). La aplicación es local-first: las ventas se registran en SQLite cuando no hay conexión y se sincronizan al reconectarse.

Flujo de venta

1

Buscar producto

Escanea el código de barras con el lector (campo siempre en foco) o escribe nombre, código o categoría en la búsqueda manual. La búsqueda aplica un debounce de 300 ms y consulta GET /api/productos?buscar=...&sucursalId=<id>.
2

Agregar al carrito

Al seleccionar un producto, se agrega una línea al carrito. Si el producto ya existe en el carrito, se incrementa la cantidad en 1. La interfaz muestra subtotal por línea en tiempo real usando precioConIva.
3

Verificar stock

El cliente comprueba el stock de la sucursal activa antes de mostrar el producto. Si stockActual === 0, el producto aparece deshabilitado y no puede agregarse. La verificación definitiva ocurre dentro de la transacción en el servidor.
4

Confirmar venta

El cajero ingresa el nombre del cliente (por defecto Consumidor Final) y confirma. La pantalla llama a POST /api/ventas con el carrito completo. El servidor crea la FacturaDte y descuenta stock en una sola transacción atómica con timeout de 10 segundos.
5

DTE generado

Tras confirmar, el servidor dispara enviarDteHacienda de forma asíncrona. La venta queda registrada inmediatamente con estado SIMULADO o PROCESADO. El cajero puede reimprimir el ticket en cualquier momento desde historial.

Validaciones

El servidor aplica dos capas de validación antes de crear la factura. Validación de tipo de unidad (tipoUnidad): garantiza que la cantidad sea coherente con la forma en que se vende el producto.
function validarCantidadPorUnidad(
  cantidad: number,
  tipoUnidad: string | null,
  nombreProducto: string,
): string | null {
  const tipo = tipoUnidad?.toUpperCase() ?? 'UNIDAD';

  if ((tipo === 'UNIDAD' || tipo === 'LOTE') && !Number.isInteger(cantidad)) {
    return `"${nombreProducto}" se vende por ${tipo} — la cantidad debe ser un número entero (recibido: ${cantidad})`;
  }
  if ((tipo === 'PESO' || tipo === 'MEDIDA') && cantidad <= 0) {
    return `"${nombreProducto}" se vende por ${tipo} — la cantidad debe ser mayor a 0`;
  }
  return null;
}
Tipo de unidadRegla de cantidad
UNIDADNúmero entero positivo
CAJA / LOTENúmero entero positivo
PESODecimal mayor que 0
MEDIDADecimal mayor que 0
La verificación de stock se realiza dentro de la transacción de base de datos (prisma.$transaction) para evitar condiciones de carrera. Si dos vendedores intentan vender el mismo artículo en simultáneo, solo uno de ellos tendrá éxito. El error devuelto es HTTP 409 con el campo detalle que lista los productos sin stock suficiente.

Cálculo de precios

Ferred aplica IVA del 13% sobre el subtotal de la venta. El cálculo se hace en el servidor antes de crear la transacción:
const subtotal    = items.reduce((acc, i) => acc + parseFloat((i.cantidad * i.precioUnit).toFixed(2)), 0);
const iva         = parseFloat((subtotal * 0.13).toFixed(2));
const total       = parseFloat((subtotal + iva).toFixed(2));
const subtotalFix = parseFloat(subtotal.toFixed(2));
Los tres valores (totalSinIva, iva, total) se almacenan por separado en el modelo FacturaDte y aparecen detallados en el ticket.

Tipos de pago

El campo tipoPago en el esquema de la venta acepta cualquier cadena de texto; el valor por defecto es efectivo.
const VentaSchema = z.object({
  sucursalId:    z.number().int().positive(),
  items:         z.array(ItemVentaSchema).min(1, 'El carrito no puede estar vacío'),
  clienteNombre: z.string().optional().default('Consumidor Final'),
  tipoPago:      z.string().optional().default('efectivo'),
});
El módulo POS actual envía siempre tipoPago: 'efectivo'. Para registrar otros medios de pago (tarjeta, transferencia, etc.), pasa el valor correspondiente al campo al llamar a POST /api/ventas.

Reimpresión de tickets

Para reimprimir un ticket desde el historial, usa el endpoint:
GET /api/ventas/:id/ticket
Roles permitidos: ADMIN, CAJERO. El cajero solo puede acceder a tickets de su propia sucursal (validado por assertSameSucursal). La respuesta incluye todos los datos necesarios para imprimir:
{
  "facturaId": 42,
  "codigoGeneracion": "550E8400-E29B-41D4-A716-446655440000",
  "numeroControl": "DTE-01-0001P001-000000000000042",
  "fecha": "2026-05-07T14:30:00.000Z",
  "sucursal": { "nombre": "Sucursal Central", "direccion": "...", "telefono": "..." },
  "cajero": "Ana García",
  "clienteNombre": "Consumidor Final",
  "tipoDte": "01",
  "estado": "PROCESADO",
  "items": [
    { "nombre": "Tornillo 1/4", "tipoUnidad": "UNIDAD", "cantidad": 10, "precioUnit": 0.15, "subtotal": 1.50 }
  ],
  "resumen": { "subtotal": 1.50, "iva": 0.20, "total": 1.70 },
  "dteJson": "{ ... }"
}

Comportamiento offline

Cuando la conexión al servidor central está caída, el módulo POS sigue operativo:
  • Las ventas se registran localmente en SQLite mediante el servicio logPendiente.
  • Los productos se sirven desde obtenerProductosSqlite() con los últimos datos conocidos.
  • Al recuperarse la conexión, el SyncService drena la cola SyncLog y sube todos los registros pendientes al servidor central.
El envío del DTE a Hacienda se dispara con setImmediate() después de devolver la respuesta HTTP al cliente. Esto significa que la confirmación de venta no espera a que Hacienda responda: la factura se crea primero con estado SIMULADO y luego pasa a PROCESADO (o ERROR_HACIENDA) de forma asíncrona. Si necesitas reenviar un DTE fallido, usa POST /api/dte/:id/reenviar.

Build docs developers (and LLMs) love