Añade revisión pre-envío del reformista y PDF de presupuesto pulido
Adelanta de F1.5 a F2 la validación pre-envío: el panel permite elegir modo de envío (automático/revisión), editar los conceptos del presupuesto y enviar al cliente por WhatsApp (simulado). Añade datos de empresa y logo configurables en /panel/empresa y genera el presupuesto como PDF real descargable con esa marca vía @react-pdf/renderer, sustituyendo la vista HTML imprimible. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
60
mvp/b2c/tests/budget/edit.test.ts
Normal file
60
mvp/b2c/tests/budget/edit.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { applyConceptoEdits, AVISO_EDITADO } from '@/budget/edit';
|
||||
import type { BudgetResult } from '@/budget/types';
|
||||
|
||||
const base: BudgetResult = {
|
||||
partidas: [
|
||||
{ key: 'demolicion', label: 'Demolición', importe: 100000 },
|
||||
{ key: 'alicatado', label: 'Alicatado y solado', importe: 200000 },
|
||||
],
|
||||
subtotal: 300000,
|
||||
factorZona: 1.2,
|
||||
total: 360000,
|
||||
rango: { min: 324000, max: 396000 },
|
||||
confianza: 'media',
|
||||
materialesRender: ['azulejo blanco'],
|
||||
avisos: ['Sin precio para pintura'],
|
||||
};
|
||||
|
||||
describe('applyConceptoEdits', () => {
|
||||
it('recalcula subtotal y total aplicando el factor de zona', () => {
|
||||
const result = applyConceptoEdits(base, [
|
||||
{ key: 'demolicion', label: 'Demolición', importe: 150000 },
|
||||
{ key: 'alicatado', label: 'Alicatado y solado', importe: 200000 },
|
||||
]);
|
||||
expect(result.subtotal).toBe(350000);
|
||||
expect(result.total).toBe(420000); // 350000 * 1.2
|
||||
});
|
||||
|
||||
it('permite añadir partidas libres y quitar existentes', () => {
|
||||
const result = applyConceptoEdits(base, [
|
||||
{ key: 'demolicion', label: 'Demolición', importe: 100000 },
|
||||
{ key: 'custom-1', label: 'Imprevistos de obra', importe: 50000 },
|
||||
]);
|
||||
expect(result.partidas).toHaveLength(2);
|
||||
expect(result.partidas[1]).toEqual({ key: 'custom-1', label: 'Imprevistos de obra', importe: 50000 });
|
||||
expect(result.subtotal).toBe(150000);
|
||||
});
|
||||
|
||||
it('descarta partidas sin etiqueta y normaliza importes (>=0, enteros)', () => {
|
||||
const result = applyConceptoEdits(base, [
|
||||
{ key: 'a', label: ' ', importe: 999 },
|
||||
{ key: 'b', label: 'Válida', importe: -500 },
|
||||
{ key: 'c', label: 'Decimal', importe: 123.7 },
|
||||
]);
|
||||
expect(result.partidas).toEqual([
|
||||
{ key: 'b', label: 'Válida', importe: 0 },
|
||||
{ key: 'c', label: 'Decimal', importe: 124 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('marca el presupuesto como ajustado a mano (aviso idempotente, confianza alta, rango = total)', () => {
|
||||
const once = applyConceptoEdits(base, base.partidas);
|
||||
expect(once.confianza).toBe('alta');
|
||||
expect(once.avisos).toContain(AVISO_EDITADO);
|
||||
expect(once.rango).toEqual({ min: once.total, max: once.total });
|
||||
|
||||
const twice = applyConceptoEdits(once, once.partidas);
|
||||
expect(twice.avisos.filter((a) => a === AVISO_EDITADO)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user