Proceso de facturación
Descripción completa del flujo de creación de facturas en Pulpo POS, desde la solicitud del cliente hasta el almacenamiento final.
Resumen del flujo
Sección titulada «Resumen del flujo»| Fase | Descripción | Acceso a BD |
|---|---|---|
| 1 | Autenticación y resolución del tenant | Lectura |
| 2 | Carga de datos de referencia (productos, impuestos) | Lectura |
| 3 | Cálculo puro con calculateInvoice() | Ninguno |
| 4 | Snapshot del cliente | Lectura |
| 5 | Determinación de serie (ticket o factura) | Ninguno |
| 6 | Transacción: bloqueo, creación, contador, stock | Lectura + Escritura |
| 7 | Respuesta con factura completa | Lectura |
Fases en detalle
Sección titulada «Fases en detalle»1. Autenticación y resolución del tenant
Sección titulada «1. Autenticación y resolución del tenant»Cada solicitud llega al endpoint POST /invoices. El sistema extrae el user_id del token de autenticación y busca el tenant asociado en la tabla directus_users.
Posibles errores:
401— Usuario no autenticado401— Usuario sin tenant asignado
2. Carga de datos de referencia
Sección titulada «2. Carga de datos de referencia»Antes de cualquier cálculo, se cargan tres conjuntos de datos de solo lectura:
a) Código postal del tenant
Sección titulada «a) Código postal del tenant»Se lee el campo postcode de la tabla tenants. Este código postal determina la zona fiscal aplicable.
b) Productos
Sección titulada «b) Productos»Se cargan todos los productos solicitados con sus campos: id, name, price_gross, tax_class.code y cost_center.name. Si algún producto no existe, se devuelve error 400.
c) Tipos impositivos
Sección titulada «c) Tipos impositivos»El sistema busca la zona fiscal cuyo patrón regex coincida con el código postal del tenant. Las zonas se evalúan ordenadas por prioridad. Una vez encontrada la zona, se cargan las tax_rules asociadas, convirtiendo las tasas de porcentaje (ej. 7.0) a decimal (ej. 0.07).
Ejemplo: Un tenant en las Islas Canarias (código postal
35XXX) coincidirá con una zona fiscal que aplica IGIC (7%) en lugar de IVA peninsular (21%).
3. Cálculo de la factura
Sección titulada «3. Cálculo de la factura»La función calculateInvoice() del paquete @pulpo/invoice realiza todos los cálculos de forma pura, sin acceso a base de datos. Esta misma función se usa tanto en el servidor como en la app del TPV, garantizando resultados idénticos.
Pasos del cálculo:
- Descuentos por línea — Para cada producto:
precio_bruto × cantidad, luego se aplica el descuento individual (porcentaje o importe fijo). El mínimo es 0. - Subtotal — Suma de todos los importes brutos de línea (después de descuentos por línea).
- Descuento global — Si existe un descuento global, se aplica al subtotal (porcentaje o importe fijo). Se calcula un
ratio de descuentopara distribuirlo proporcionalmente entre las líneas. - Cálculo inverso de impuestos — Los precios son brutos (impuestos incluidos). El neto se obtiene dividiendo entre
(1 + tipoImpositivo). Se redondea a 8 decimales para máxima precisión. - Desglose de impuestos — Se agrupan las líneas por tipo impositivo y se calcula el neto y el impuesto por cada grupo. El resultado se ordena de menor a mayor tasa.
Aritmética decimal
Sección titulada «Aritmética decimal»Todos los valores monetarios se manejan como string y la aritmética interna utiliza big.js para evitar errores de punto flotante.
| Tipo de valor | Precisión | Ejemplo |
|---|---|---|
| Totales monetarios | 2 decimales | "12.50" |
| Precios unitarios | 4 decimales | "3.5000" |
| Neto preciso | 8 decimales | "3.27102804" |
| Tipo impositivo (snapshot) | 2 decimales (%) | "7.00" |
4. Snapshot del cliente
Sección titulada «4. Snapshot del cliente»Si se proporciona un customer_id, se copian los datos actuales del cliente (nombre, NIF, dirección, email, teléfono) directamente en la factura. Esto garantiza que la factura conserve los datos del cliente tal como eran en el momento de la venta, incluso si el cliente modifica sus datos posteriormente.
Si el cliente no se encuentra en la base de datos, se continúa sin snapshot pero manteniendo la referencia al ID.
5. Determinación de la serie
Sección titulada «5. Determinación de la serie»El tipo de documento se determina automáticamente según si hay un cliente asociado:
| Condición | Serie | Prefijo | Contador |
|---|---|---|---|
Sin customer_id | ticket | (ninguno) | last_ticket_number |
Con customer_id | factura | F- | last_factura_number |
6. Transacción con bloqueo (escritura atómica)
Sección titulada «6. Transacción con bloqueo (escritura atómica)»Todas las operaciones de escritura se ejecutan dentro de una transacción de base de datos. Si algún paso falla, no se guardan cambios parciales.
a) Bloqueo del tenant (FOR UPDATE)
Sección titulada «a) Bloqueo del tenant (FOR UPDATE)»Se bloquea la fila del tenant con SELECT ... FOR UPDATE. Esto serializa todas las solicitudes concurrentes del mismo tenant, evitando la generación de números de factura duplicados.
b) Generación del número de factura
Sección titulada «b) Generación del número de factura»Se lee el registro del tenant (dentro del bloqueo) y se genera el número usando la función generateInvoiceNumber().
Formato del número: [serie]-[prefijo con variables]
Variables disponibles en el prefijo:
| Variable | Ejemplo |
|---|---|
%date% | 20260227 |
%year% | 2026 |
%month% | 02 |
%day% | 27 |
%count% | 00042 (5 dígitos, con ceros) |
Ejemplo completo: F-2026-00042
c) Verificación de caja abierta
Sección titulada «c) Verificación de caja abierta»Se busca un registro en cash_register_closures con estado "open" para el tenant. Sin una caja abierta, no se puede crear la factura (error NO_OPEN_CLOSURE).
d) Creación del registro de factura
Sección titulada «d) Creación del registro de factura»Se crea la factura con todas las relaciones en una sola operación de Directus:
- Datos principales: número, serie, totales, desglose fiscal
- Snapshot del emisor: nombre, NIF, dirección del tenant
- Snapshot del cliente (si aplica)
- Líneas de factura — creación anidada con
items: { create: [...] } - Pagos — creación anidada con
payments: { create: [...] }
e) Actualización del contador
Sección titulada «e) Actualización del contador»Se incrementa el contador correspondiente en el tenant: last_ticket_number o last_factura_number, según la serie.
f) Reducción de stock
Sección titulada «f) Reducción de stock»Para cada producto vendido que tenga control de stock (stock IS NOT NULL), se reduce la cantidad. Se usa GREATEST(stock - cantidad, 0) para evitar valores negativos.
7. Respuesta
Sección titulada «7. Respuesta»Una vez completada la transacción, se lee la factura completa con todas sus relaciones (items y payments) y se devuelve al cliente.
{ "success": true, "invoice": { "id": "uuid", "invoice_number": "F-2026-00042", "invoice_type": "factura", "status": "paid", "total_net": "10.28", "total_tax": "0.72", "total_gross": "11.00", "tax_breakdown": [ { "rate": "7.00", "net": "10.28", "tax": "0.72" } ], "items": ["..."], "payments": ["..."] }}Estructura de datos
Sección titulada «Estructura de datos»Cuerpo de la solicitud (Request Body)
Sección titulada «Cuerpo de la solicitud (Request Body)»| Campo | Tipo | Descripción |
|---|---|---|
status | "paid" | Estado de la factura |
items | RequestItem[] | Líneas de producto |
items[].product_id | string | ID del producto |
items[].quantity | number | Cantidad |
items[].discount | Discount? | Descuento por línea (opcional) |
discount | Discount? | Descuento global (opcional) |
customer_id | string? | ID del cliente (determina si es factura o ticket) |
payments | Payment[] | Métodos de pago utilizados |
payments[].method | "cash" | "card" | Método de pago |
payments[].amount | string | Importe del pago |
payments[].tendered | string? | Efectivo entregado (solo cash) |
payments[].change | string? | Cambio devuelto (solo cash) |
payments[].tip | string? | Propina |
Tipo de descuento (Discount)
Sección titulada «Tipo de descuento (Discount)»| Campo | Tipo | Descripción |
|---|---|---|
type | "percent" | "fixed" | Porcentaje o importe fijo |
value | number | Valor del descuento (ej. 10 para 10% o 10 EUR) |
Tablas de base de datos
Sección titulada «Tablas de base de datos»Tablas involucradas en el proceso de facturación y su papel:
| Tabla | Acceso | Propósito |
|---|---|---|
directus_users | Lectura | Resolver tenant del usuario |
tenants | Lectura + Bloqueo | Código postal, contadores, datos del emisor |
products | Lectura + Escritura (stock) | Datos del producto, reducción de stock |
tax_zones | Lectura | Zonas fiscales con regex de código postal |
tax_rules | Lectura | Tipos impositivos por zona y clase |
customers | Lectura | Datos del cliente para snapshot |
cash_register_closures | Lectura | Verificar que la caja esté abierta |
invoices | Escritura | Registro principal de la factura |
invoice_items | Escritura | Líneas de la factura (creación anidada) |
invoice_payments | Escritura | Pagos de la factura (creación anidada) |
Manejo de errores
Sección titulada «Manejo de errores»| Código | Error | Causa |
|---|---|---|
401 | Nicht authentifiziert | No se proporcionó token de autenticación o es inválido |
401 | Kein Tenant zugewiesen | El usuario no tiene un tenant asignado |
400 | Produkt nicht gefunden | Uno o más productos no existen en la base de datos |
500 | NO_OPEN_CLOSURE | No hay una caja registradora abierta para el tenant |
500 | Tenant nicht gefunden | El tenant fue eliminado durante la transacción |