From 4ef79f3f9fc60789cd00317827c5bb12621a2a5b Mon Sep 17 00:00:00 2001 From: mono Date: Sun, 25 Oct 2026 18:46:01 -0500 Subject: [PATCH] test: cubrir casos de uso operativos --- .../__tests__/use-cases.integration.test.ts | 111 ++++++++++++++++++ .../inventario/ImportInventarioCsvService.ts | 2 +- 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 apps/toldo-app/src/__tests__/use-cases.integration.test.ts diff --git a/apps/toldo-app/src/__tests__/use-cases.integration.test.ts b/apps/toldo-app/src/__tests__/use-cases.integration.test.ts new file mode 100644 index 0000000..9853b37 --- /dev/null +++ b/apps/toldo-app/src/__tests__/use-cases.integration.test.ts @@ -0,0 +1,111 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { db } from '@toldo/data'; +import { + AuditoriaService, + CuadreService, + EdicionService, + ImportInventarioCsvService, + JornadaService, + ReportesService, +} from '@toldo/services'; + +const fecha = new Date('2026-01-15T10:00:00'); + +async function prepararBase() { + await db.delete(); + await db.open(); +} + +async function crearProducto(id = 'producto-1', cantidad = 10) { + await db.productores.add({ id: 'productor-1', nombre: 'Productor Uno', activo: true, createdAt: fecha } as any); + await db.productos.add({ id, productorId: 'productor-1', nombrePresentacion: 'Cafe 250g', precioUnitario: 10000, activo: true, createdAt: fecha } as any); + await db.movimientosInventario.add({ id: `stock-${id}`, fecha, tipo: 'INGRESO_INICIAL', productoId: id, cantidad, createdAt: fecha } as any); +} + +describe('casos de uso operativos offline', () => { + beforeEach(prepararBase); + + it('impide dos jornadas abiertas y no permite operar una jornada cerrada', async () => { + const service = new JornadaService(); + const jornadaId = await service.abrirJornada(['Ana'], 50000); + await expect(service.abrirJornada(['Luis'], 10000)).rejects.toThrow('Ya existe una jornada abierta'); + await service.cerrarJornada(jornadaId); + await expect(service.registrarGastoEnJornada(jornadaId, { id: 'gasto-cerrado', fecha, concepto: 'Bolsas', monto: 1000, formaPago: 'EFECTIVO', createdAt: fecha } as any)).rejects.toThrow('jornada cerrada'); + }); + + it('calcula cuadre mixto de efectivo y QR y registra ambos descuadres', async () => { + await crearProducto(); + const jornada = new JornadaService(); + const jornadaId = await jornada.abrirJornada(['Ana'], 50000); + const ventaEfectivo = crypto.randomUUID(); + await jornada.registrarVentaEnJornada(jornadaId, { id: ventaEfectivo, fecha, subtotal: 20000, total: 20000, createdAt: fecha } as any, [{ id: crypto.randomUUID(), ventaId: ventaEfectivo, productoId: 'producto-1', cantidad: 2, precioUnitario: 10000, subtotal: 20000, formaPago: 'EFECTIVO' } as any]); + const ventaQr = crypto.randomUUID(); + await jornada.registrarVentaEnJornada(jornadaId, { id: ventaQr, fecha, subtotal: 10000, total: 10000, createdAt: fecha } as any, [{ id: crypto.randomUUID(), ventaId: ventaQr, productoId: 'producto-1', cantidad: 1, precioUnitario: 10000, subtotal: 10000, formaPago: 'QR' } as any]); + await jornada.registrarGastoEnJornada(jornadaId, { id: 'gasto-efectivo', fecha, concepto: 'Bolsas', monto: 5000, formaPago: 'EFECTIVO', createdAt: fecha } as any); + await jornada.registrarGastoEnJornada(jornadaId, { id: 'gasto-qr', fecha, concepto: 'Transporte', monto: 2000, formaPago: 'QR', createdAt: fecha } as any); + + const cuadreService = new CuadreService(); + const calculado = await cuadreService.calcularCuadreDelDia(fecha, 50000); + expect(calculado.totalVentasEfectivo).toBe(20000); + expect(calculado.totalVentasQR).toBe(10000); + expect(calculado.saldoEsperadoEfectivo).toBe(65000); + const cuadre: any = { ...calculado, id: 'cuadre-1', saldoRealEfectivo: 64000, totalQRBanco: 9000, createdAt: fecha }; + await cuadreService.guardarCuadre(cuadre); + const guardado = await db.cuadres.get('cuadre-1'); + expect(guardado?.descuadreEfectivo).toBe(-1000); + expect(guardado?.totalQRRegistrado).toBe(8000); + expect(guardado?.descuadreQR).toBe(1000); + }); + + it('rechaza stock insuficiente y revierte venta y movimiento parcial', async () => { + await crearProducto('producto-escaso', 1); + const jornada = new JornadaService(); + const jornadaId = await jornada.abrirJornada(['Ana'], 0); + const ventaId = crypto.randomUUID(); + await expect(jornada.registrarVentaEnJornada(jornadaId, { id: ventaId, fecha, subtotal: 20000, total: 20000, createdAt: fecha } as any, [{ id: crypto.randomUUID(), ventaId, productoId: 'producto-escaso', cantidad: 2, precioUnitario: 10000, subtotal: 20000, formaPago: 'EFECTIVO' } as any])).rejects.toThrow('Stock insuficiente'); + expect(await db.ventas.count()).toBe(0); + expect((await db.movimientosInventario.toArray()).filter((movimiento) => movimiento.referenciaId === ventaId)).toHaveLength(0); + }); + + it('importa CSV con normalización, cantidades vacías y errores por fila', async () => { + const importer = new ImportInventarioCsvService(); + const resultado = await importer.importar('Nombre de producto,Nombre Productor,precio unitario,cantidad\nCafe 250g,Ángel Uno,10000,3\ncafe 250g, angel uno ,10000,\nProducto roto,Otro,precio,2'); + expect(await db.productores.count()).toBe(1); + expect(await db.productos.count()).toBe(1); + expect(await db.movimientosInventario.count()).toBe(1); + expect(resultado.movimientos).toBe(1); + expect(resultado.errores).toHaveLength(1); + }); + + it('rechaza CSV sin la columna cantidad', async () => { + await expect(new ImportInventarioCsvService().importar('Nombre de producto,Nombre Productor,precio unitario\nCafe,Productor,1000')).rejects.toThrow('cantidad'); + }); + + it('audita edición de producto y rechaza edición sin motivo', async () => { + await crearProducto(); + const editor = new EdicionService(); + const original = await db.productos.get('producto-1') as any; + await expect(editor.editarProducto({ ...original, precioUnitario: 12000 }, original, { modificadoPor: 'Ana', motivo: '' })).rejects.toThrow('motivo'); + await editor.editarProducto({ ...original, precioUnitario: 12000 }, original, { modificadoPor: 'Ana', motivo: 'Precio actualizado' }); + const auditoria = await new AuditoriaService().listar('PRODUCTO'); + expect(auditoria).toHaveLength(1); + expect(JSON.parse(auditoria[0].valoresNuevos).precioUnitario).toBe(12000); + }); + + it('genera reporte por productor y consolidado con ventas y ajustes', async () => { + await crearProducto(); + const jornada = new JornadaService(); + const jornadaId = await jornada.abrirJornada(['Ana'], 0); + const ventaId = crypto.randomUUID(); + await jornada.registrarVentaEnJornada(jornadaId, { id: ventaId, fecha, subtotal: 10000, total: 10000, createdAt: fecha } as any, [{ id: crypto.randomUUID(), ventaId, productoId: 'producto-1', cantidad: 1, precioUnitario: 10000, subtotal: 10000, formaPago: 'EFECTIVO' } as any]); + await db.ajustes.add({ id: 'ajuste-1', fecha, productoId: 'producto-1', cantidadAnterior: 9, cantidadNueva: 8, diferencia: -1, motivo: 'Conteo físico', createdAt: fecha } as any); + const reportes = new ReportesService(); + const porProductor = await reportes.reportePorProductor(fecha); + expect(porProductor[0].totalVentas).toBe(10000); + expect(porProductor[0].inventarioInicial).toBe(10); + expect(porProductor[0].diferenciaInventario).toBe(-1); + const consolidado = await reportes.consolidadoPorRango(new Date('2026-01-15T00:00:00'), new Date('2026-01-15T23:59:59')); + expect(consolidado.ventasTotal).toBe(10000); + expect(consolidado.porProductor[0].totalVentas).toBe(10000); + }); +}); diff --git a/packages/services/src/inventario/ImportInventarioCsvService.ts b/packages/services/src/inventario/ImportInventarioCsvService.ts index 51c5481..a14cc93 100644 --- a/packages/services/src/inventario/ImportInventarioCsvService.ts +++ b/packages/services/src/inventario/ImportInventarioCsvService.ts @@ -26,7 +26,7 @@ export class ImportInventarioCsvService { const encabezados = separar(filas[0]).map(normalizarNombre); const requeridos = ['nombre de producto', 'nombre productor', 'precio unitario', 'cantidad']; const indices = requeridos.map((nombre) => encabezados.indexOf(normalizarNombre(nombre))); - if (indices.slice(0, 3).some((indice) => indice < 0)) throw new Error('El CSV debe incluir: Nombre de producto, Nombre Productor y precio unitario.'); + if (indices.some((indice) => indice < 0)) throw new Error('El CSV debe incluir: Nombre de producto, Nombre Productor, precio unitario y cantidad.'); await db.transaction('rw', db.productores, db.productos, db.movimientosInventario, async () => { const productores = await db.productores.toArray();