const { parseNumber, isRentalDuplicate, parseWorkbook } = require('../src/services/smartPackageExcelImportService'); const XLSX = require('xlsx'); const fs = require('fs'); const os = require('os'); const path = require('path'); describe('SmartPackageExcelImportService', () => { test.each([ ['1,007.97 kr.', 1007.97], ['1.007,97 kr.', 1007.97], ['600.00 kr.', 600], ['0.20', 0.2], [null, 0] ])('parses Excel number %p', (value, expected) => { expect(parseNumber(value)).toBe(expected); }); test('removes zero-price rental duplicates covered by Byggeplads', () => { const buildingSite = [ { name: 'Leje af minigraver 1,5-2,0 ton' }, { name: 'Mandskabsvogn 4 personer' }, { name: 'Køreplader i stål' }, { name: 'Leje Affaldscontainer åben 20 m³' } ]; expect(isRentalDuplicate({ name: 'Minigraver 1,8 ton', standardPrice: 0 }, buildingSite)).toBe(true); expect(isRentalDuplicate({ name: 'Mandskabsvogn 4 pers.', standardPrice: 0 }, buildingSite)).toBe(true); expect(isRentalDuplicate({ name: 'Køreplader stål', standardPrice: 0 }, buildingSite)).toBe(true); expect(isRentalDuplicate({ name: '20 m3 åben', standardPrice: 0 }, buildingSite)).toBe(true); expect(isRentalDuplicate({ name: '10 m3 åben', standardPrice: 0 }, buildingSite)).toBe(false); expect(isRentalDuplicate({ name: '20 m3 lukket', standardPrice: 0 }, buildingSite)).toBe(false); }); test('keeps priced and non-overlapping rental packages', () => { const buildingSite = [{ name: 'Leje af saxlift 6 m' }]; expect(isRentalDuplicate({ name: 'Saxlift 6 m', standardPrice: 320 }, buildingSite)).toBe(false); expect(isRentalDuplicate({ name: 'Saxlift 10 m', standardPrice: 0 }, buildingSite)).toBe(false); }); test('maps an unknown workbook layout to canonical SmartPackage fields', () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-map-')); const filePath = path.join(directory, 'custom.xlsx'); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet([ { Ydelse: 'Opsætning af skur', Gruppe: 'Byggeplads', Pris: '1.250,00', Måleenhed: 'stk.' } ]), 'Ark 1'); XLSX.writeFile(workbook, filePath); const parsed = parseWorkbook(filePath, { sheets: [{ sourceSheet: 'Ark 1', targetSheet: 'Byggeplads', columns: [ { source: 'Ydelse', target: 'name' }, { source: 'Gruppe', target: 'category' }, { source: 'Pris', target: 'unitPrice' }, { source: 'Måleenhed', target: 'unit' } ] }] }); expect(parsed.packages).toHaveLength(1); expect(parsed.packages[0]).toMatchObject({ name: 'Opsætning af skur', effectivePrice: 1250, unit: 'stk.' }); fs.rmSync(directory, { recursive: true, force: true }); }); test('rejects mappings without a name column', () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-map-')); const filePath = path.join(directory, 'custom.xlsx'); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet([{ Pris: 100 }]), 'Data'); XLSX.writeFile(workbook, filePath); expect(() => parseWorkbook(filePath, { sheets: [{ sourceSheet: 'Data', targetSheet: 'Byggeplads', columns: [{ source: 'Pris', target: 'unitPrice' }] }] })).toThrow('mangler pakkenavn'); fs.rmSync(directory, { recursive: true, force: true }); }); test('preserves the real Excel sheet and row when blank rows occur', () => { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'excel-row-')); const filePath = path.join(directory, 'custom.xlsx'); const workbook = XLSX.utils.book_new(); const sheet = XLSX.utils.aoa_to_sheet([ ['Ydelse', 'Pris'], [], ['Skurvogn', 750] ]); XLSX.utils.book_append_sheet(workbook, sheet, 'Importdata'); XLSX.writeFile(workbook, filePath); const parsed = parseWorkbook(filePath, { sheets: [{ sourceSheet: 'Importdata', targetSheet: 'Udlejning', columns: [ { source: 'Ydelse', target: 'name' }, { source: 'Pris', target: 'standardPrice' } ] }] }); expect(parsed.packages[0]).toMatchObject({ sourceSheet: 'Udlejning', excelSheet: 'Importdata', sourceRow: 3 }); fs.rmSync(directory, { recursive: true, force: true }); }); });