feat: add catalog CSV parser with per-row validation

This commit is contained in:
Carlos Narro
2026-05-30 12:24:47 +02:00
parent 896c7ac89b
commit 58d3f62a76
3 changed files with 122 additions and 1 deletions

View File

@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import { parseCatalogCsv } from '@/budget/csv';
const HEADER = 'categoria,nombre,calidad,precio,unidad,descriptor_render,sku';
describe('parseCatalogCsv', () => {
it('parsea filas válidas y convierte precio a céntimos', () => {
const csv = [
HEADER,
'suelo,Cerámico gris,media,28.00,m2,suelo cerámico gris,SUE-M',
'mobiliario,Muebles cocina,premium,550,ml,muebles laminado roble,MOB-P',
].join('\n');
const { rows, errors } = parseCatalogCsv(csv);
expect(errors).toHaveLength(0);
expect(rows).toHaveLength(2);
expect(rows[0]).toMatchObject({
categoria: 'suelo',
calidad: 'media',
precioUnit: 2800,
unidad: 'm2',
sku: 'SUE-M',
});
expect(rows[1].precioUnit).toBe(55000);
});
it('reporta errores por fila sin abortar las válidas', () => {
const csv = [
HEADER,
'suelo,Bueno,media,28,m2,desc,SUE-M',
'inventada,Malo,media,10,m2,desc,X', // categoria inválida
'pared,Sin precio,media,abc,m2,desc,PAR-M', // precio no numérico
].join('\n');
const { rows, errors } = parseCatalogCsv(csv);
expect(rows).toHaveLength(1);
expect(errors).toHaveLength(2);
expect(errors[0].line).toBe(3); // 1-indexed incluyendo cabecera
expect(errors[1].line).toBe(4);
});
it('devuelve error global si falta la cabecera esperada', () => {
const { rows, errors } = parseCatalogCsv('a,b,c\n1,2,3');
expect(rows).toHaveLength(0);
expect(errors[0].message).toMatch(/cabecera/i);
});
});