feat: implementar jornadas y validar aliases

This commit is contained in:
mono
2026-10-25 17:20:48 -05:00
parent 89390b5bed
commit 4ffce3e3cd
27 changed files with 537 additions and 29 deletions

View File

@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest';
import { puedeAbrirJornada, puedeCerrarJornada, validarApertura, calcularSaldoJornada } from '../rules/jornadaRules';
describe('jornadaRules', () => {
it('no permite abrir si ya hay jornada abierta', () => {
expect(puedeAbrirJornada(1)).toBe(false);
});
it('permite abrir si no hay jornada abierta', () => {
expect(puedeAbrirJornada(0)).toBe(true);
});
it('permite cerrar solo si está abierta', () => {
expect(puedeCerrarJornada('ABIERTA')).toBe(true);
expect(puedeCerrarJornada('CERRADA')).toBe(false);
});
it('valida apertura requiere responsables y base >=0', () => {
expect(validarApertura([], 100)).toBe('Debe haber al menos un responsable');
expect(validarApertura(['Ana'], -1)).toBe('Base inicial no puede ser negativa');
expect(validarApertura(['Ana'], 0)).toBe(null);
});
it('calcula saldo de jornada', () => {
expect(calcularSaldoJornada(100000, 500000, 50000)).toBe(550000);
});
});

View File

@@ -0,0 +1,19 @@
export type EstadoJornada = 'ABIERTA' | 'CERRADA';
export interface JornadaToldo {
id: string;
fecha: Date;
estado: EstadoJornada;
responsables: string[];
baseInicial: number;
fechaApertura: Date;
fechaCierre?: Date;
inventarioInicialIds?: string[];
inventarioFinalIds?: string[];
ventaIds: string[];
gastoIds: string[];
ajusteIds: string[];
cuadreId?: string;
createdAt: Date;
updatedAt?: Date;
}

View File

@@ -5,3 +5,4 @@ export * from './Venta';
export * from './Gasto';
export * from './Ajuste';
export * from './CuadreCaja';
export * from './JornadaToldo';

View File

@@ -1,2 +1,4 @@
export * from './entities';
export * from './rules/cuadreRules';
export * from './rules/jornadaRules';
export * from './entities/JornadaToldo';

View File

@@ -0,0 +1,21 @@
export function puedeAbrirJornada(jornadasAbiertas: number): boolean {
return jornadasAbiertas === 0;
}
export function puedeCerrarJornada(estado: string): boolean {
return estado === 'ABIERTA';
}
export function validarApertura(responsables: string[], baseInicial: number): string | null {
if (!responsables || responsables.length === 0) return 'Debe haber al menos un responsable';
if (baseInicial < 0) return 'Base inicial no puede ser negativa';
return null;
}
export function calcularSaldoJornada(
baseInicial: number,
totalVentas: number,
totalGastos: number
): number {
return baseInicial + totalVentas - totalGastos;
}

View File

@@ -1,5 +1,5 @@
import Dexie, { type Table } from 'dexie';
import type { Productor, Producto, MovimientoInventario, Venta, VentaLinea, Gasto, Ajuste, CuadreCaja } from '@toldo/core';
import type { Productor, Producto, MovimientoInventario, Venta, VentaLinea, Gasto, Ajuste, CuadreCaja, JornadaToldo } from '@toldo/core';
export class ToldoDB extends Dexie {
productores!: Table<Productor, string>;
@@ -10,6 +10,7 @@ export class ToldoDB extends Dexie {
gastos!: Table<Gasto, string>;
ajustes!: Table<Ajuste, string>;
cuadres!: Table<CuadreCaja, string>;
jornadas!: Table<JornadaToldo, string>;
constructor() {
super('ToldoConjuntoDB');
@@ -23,6 +24,17 @@ export class ToldoDB extends Dexie {
ajustes: 'id, fecha, productoId',
cuadres: 'id, fecha'
});
this.version(2).stores({
productores: 'id, nombre, activo',
productos: 'id, productorId, nombrePresentacion, [productorId+nombrePresentacion]',
movimientosInventario: 'id, fecha, tipo, productoId',
ventas: 'id, fecha',
ventaLineas: 'id, ventaId, productoId, formaPago',
gastos: 'id, fecha',
ajustes: 'id, fecha, productoId',
cuadres: 'id, fecha',
jornadas: 'id, fecha, estado'
});
}
}

View File

@@ -0,0 +1,40 @@
import { db } from '../db/schema';
import type { JornadaToldo } from '@toldo/core';
export class JornadaRepository {
async getAll(): Promise<JornadaToldo[]> {
return db.jornadas.orderBy('fecha').reverse().toArray();
}
async getAbierta(): Promise<JornadaToldo | undefined> {
return db.jornadas.where('estado').equals('ABIERTA').first();
}
async getById(id: string): Promise<JornadaToldo | undefined> {
return db.jornadas.get(id);
}
async create(jornada: JornadaToldo): Promise<string> {
// Solo puede haber una jornada abierta
const abierta = await this.getAbierta();
if (abierta) throw new Error('Ya existe una jornada abierta. Debe cerrarla primero.');
return db.jornadas.add(jornada);
}
async update(jornada: JornadaToldo): Promise<number> {
return db.jornadas.put({ ...jornada, updatedAt: new Date() });
}
async cerrar(id: string, cuadreId: string): Promise<number> {
const jornada = await this.getById(id);
if (!jornada) throw new Error('Jornada no encontrada');
if (jornada.estado === 'CERRADA') throw new Error('Jornada ya cerrada');
return db.jornadas.put({
...jornada,
estado: 'CERRADA',
fechaCierre: new Date(),
cuadreId,
updatedAt: new Date()
});
}
}

View File

@@ -5,3 +5,4 @@ export * from './GastoRepository';
export * from './AjusteRepository';
export * from './MovimientoInventarioRepository';
export * from './CuadreRepository';
export * from './JornadaRepository';

View File

@@ -0,0 +1,105 @@
import { db } from '@toldo/data';
import type { JornadaToldo, Venta, VentaLinea, Gasto, CuadreCaja } from '@toldo/core';
import { validarApertura, puedeAbrirJornada } from '@toldo/core';
import { VentasService } from './ventas/VentasService';
import { GastosService } from './gastos/GastosService';
import { CuadreService } from './cuadre/CuadreService';
export class JornadaService {
private ventasService = new VentasService();
private gastosService = new GastosService();
private cuadreService = new CuadreService();
async getJornadaAbierta(): Promise<JornadaToldo | undefined> {
return db.jornadas.where('estado').equals('ABIERTA').first();
}
async abrirJornada(responsables: string[], baseInicial: number): Promise<string> {
const err = validarApertura(responsables, baseInicial);
if (err) throw new Error(err);
const abierta = await this.getJornadaAbierta();
if (!puedeAbrirJornada(abierta ? 1 : 0)) throw new Error('Ya existe una jornada abierta');
const jornada: JornadaToldo = {
id: crypto.randomUUID(),
fecha: new Date(),
estado: 'ABIERTA',
responsables,
baseInicial,
fechaApertura: new Date(),
ventaIds: [],
gastoIds: [],
ajusteIds: [],
createdAt: new Date()
};
return db.jornadas.add(jornada as any);
}
async registrarVentaEnJornada(jornadaId: string, venta: Venta, lineas: VentaLinea[]): Promise<string> {
const jornada: any = await db.jornadas.get(jornadaId);
if (!jornada) throw new Error('Jornada no encontrada');
if (jornada.estado !== 'ABIERTA') throw new Error('No se pueden registrar ventas en jornada cerrada');
const ventaId = await this.ventasService.registrarVenta({ ...venta, id: venta.id }, lineas);
await db.jornadas.put({
...jornada,
ventaIds: [...(jornada.ventaIds || []), venta.id],
updatedAt: new Date()
});
return ventaId;
}
async registrarGastoEnJornada(jornadaId: string, gasto: Gasto): Promise<string> {
const jornada: any = await db.jornadas.get(jornadaId);
if (!jornada) throw new Error('Jornada no encontrada');
if (jornada.estado !== 'ABIERTA') throw new Error('No se pueden registrar gastos en jornada cerrada');
const gastoId = await this.gastosService.registrarGasto(gasto);
await db.jornadas.put({
...jornada,
gastoIds: [...(jornada.gastoIds || []), gasto.id],
updatedAt: new Date()
});
return gastoId;
}
async cerrarJornada(jornadaId: string, cuadreId?: string): Promise<string> {
const jornada: any = await db.jornadas.get(jornadaId);
if (!jornada) throw new Error('Jornada no encontrada');
if (jornada.estado === 'CERRADA') throw new Error('Jornada ya cerrada');
let cid = cuadreId;
if (!cid) {
const cuadreParcial = await this.cuadreService.calcularCuadreDelDia(jornada.fecha, jornada.baseInicial);
const cuadre: CuadreCaja = {
id: crypto.randomUUID(),
fecha: jornada.fecha,
baseInicial: jornada.baseInicial,
totalVentasEfectivo: cuadreParcial.totalVentasEfectivo || 0,
totalVentasQR: cuadreParcial.totalVentasQR || 0,
totalGastosEfectivo: cuadreParcial.totalGastosEfectivo || 0,
totalGastosQR: cuadreParcial.totalGastosQR || 0,
saldoEsperadoEfectivo: cuadreParcial.saldoEsperadoEfectivo || 0,
totalVentas: cuadreParcial.totalVentas || 0,
createdAt: new Date()
} as CuadreCaja;
cid = await this.cuadreService.guardarCuadre(cuadre);
}
await db.jornadas.put({
...jornada,
estado: 'CERRADA',
fechaCierre: new Date(),
cuadreId: cid,
updatedAt: new Date()
});
return cid;
}
async getResumenJornada(jornadaId: string) {
const jornada: any = await db.jornadas.get(jornadaId);
if (!jornada) return null;
const ventas = jornada.ventaIds?.length ? await db.ventas.where('id').anyOf(jornada.ventaIds).toArray() : [];
const gastos = jornada.gastoIds?.length ? await db.gastos.where('id').anyOf(jornada.gastoIds).toArray() : [];
const totalVentas = ventas.reduce((acc: number, v: any) => acc + v.total, 0);
const totalGastos = gastos.reduce((acc: number, g: any) => acc + g.monto, 0);
return { jornada, ventas, gastos, totalVentas, totalGastos };
}
}

View File

@@ -0,0 +1,39 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { JornadaService } from '../JornadaService';
import { db } from '@toldo/data';
describe('Caso de uso jornada', () => {
const service = new JornadaService();
beforeEach(async () => {
await db.jornadas.clear();
await db.ventas.clear();
await db.ventaLineas.clear();
await db.gastos.clear();
await db.cuadres.clear();
await db.productos.clear();
});
it('abrir jornada, registrar venta y gasto, cerrar con cuadre', async () => {
await db.productos.add({ id: 'p1', productorId: 'pr1', nombrePresentacion: 'Café 500g', precioUnitario: 15000, activo: true } as any);
const jid = await service.abrirJornada(['Ana', 'Luis'], 100000);
expect(jid).toBeTruthy();
const ventaId = crypto.randomUUID();
await service.registrarVentaEnJornada(jid, { id: ventaId, fecha: new Date(), subtotal: 30000, total: 30000, createdAt: new Date() } as any, [
{ id: crypto.randomUUID(), ventaId, productoId: 'p1', cantidad: 2, precioUnitario: 15000, subtotal: 30000, formaPago: 'EFECTIVO' } as any
]);
await service.registrarGastoEnJornada(jid, { id: crypto.randomUUID(), fecha: new Date(), concepto: 'Bolsas', monto: 5000, formaPago: 'EFECTIVO', createdAt: new Date() } as any);
const resumen = await service.getResumenJornada(jid);
expect(resumen?.totalVentas).toBe(30000);
expect(resumen?.totalGastos).toBe(5000);
const cuadreId = await service.cerrarJornada(jid);
expect(cuadreId).toBeTruthy();
const abierta = await service.getJornadaAbierta();
expect(abierta).toBeUndefined();
});
});

View File

@@ -5,3 +5,4 @@ export * from './inventario/InventarioService';
export * from './cuadre/CuadreService';
export * from './reportes/ReportesService';
export * from './export-import/ExportImportService';
export * from './JornadaService';