test: cubrir casos de uso operativos
This commit is contained in:
111
apps/toldo-app/src/__tests__/use-cases.integration.test.ts
Normal file
111
apps/toldo-app/src/__tests__/use-cases.integration.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,7 +26,7 @@ export class ImportInventarioCsvService {
|
|||||||
const encabezados = separar(filas[0]).map(normalizarNombre);
|
const encabezados = separar(filas[0]).map(normalizarNombre);
|
||||||
const requeridos = ['nombre de producto', 'nombre productor', 'precio unitario', 'cantidad'];
|
const requeridos = ['nombre de producto', 'nombre productor', 'precio unitario', 'cantidad'];
|
||||||
const indices = requeridos.map((nombre) => encabezados.indexOf(normalizarNombre(nombre)));
|
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 () => {
|
await db.transaction('rw', db.productores, db.productos, db.movimientosInventario, async () => {
|
||||||
const productores = await db.productores.toArray();
|
const productores = await db.productores.toArray();
|
||||||
|
|||||||
Reference in New Issue
Block a user