Initial commit: Inventory Barcode System
This commit is contained in:
594
__tests__/ExcelImportService.test.js
Normal file
594
__tests__/ExcelImportService.test.js
Normal file
@ -0,0 +1,594 @@
|
||||
const ExcelImportService = require('../services/ExcelImportService');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
describe('ExcelImportService', () => {
|
||||
let service;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new ExcelImportService();
|
||||
});
|
||||
|
||||
describe('Column Detection', () => {
|
||||
test('should detect standard column names', () => {
|
||||
const headers = ['Product Code', 'Description', 'Quantity', 'Category'];
|
||||
const mapping = service.detectColumns(headers);
|
||||
|
||||
expect(mapping.productCode).toBe(0);
|
||||
expect(mapping.description).toBe(1);
|
||||
expect(mapping.quantity).toBe(2);
|
||||
expect(mapping.category).toBe(3);
|
||||
});
|
||||
|
||||
test('should detect case-insensitive column names', () => {
|
||||
const headers = ['PRODUCT_CODE', 'desc', 'QTY', 'cat'];
|
||||
const mapping = service.detectColumns(headers);
|
||||
|
||||
expect(mapping.productCode).toBe(0);
|
||||
expect(mapping.description).toBe(1);
|
||||
expect(mapping.quantity).toBe(2);
|
||||
expect(mapping.category).toBe(3);
|
||||
});
|
||||
|
||||
test('should detect alternative column names', () => {
|
||||
const headers = ['SKU', 'Item Name', 'Stock Level', 'Type'];
|
||||
const mapping = service.detectColumns(headers);
|
||||
|
||||
expect(mapping.productCode).toBe(0);
|
||||
expect(mapping.description).toBe(1);
|
||||
expect(mapping.quantity).toBe(2);
|
||||
expect(mapping.category).toBe(3);
|
||||
});
|
||||
|
||||
test('should handle missing columns', () => {
|
||||
const headers = ['Product Code', 'Description'];
|
||||
const mapping = service.detectColumns(headers);
|
||||
|
||||
expect(mapping.productCode).toBe(0);
|
||||
expect(mapping.description).toBe(1);
|
||||
expect(mapping.quantity).toBe(null);
|
||||
expect(mapping.category).toBe(null);
|
||||
});
|
||||
|
||||
test('should handle partial matches', () => {
|
||||
const headers = ['Item_Code_Number', 'Product_Description_Text', 'Current_Quantity_Level'];
|
||||
const mapping = service.detectColumns(headers);
|
||||
|
||||
expect(mapping.productCode).toBe(0);
|
||||
expect(mapping.description).toBe(1);
|
||||
expect(mapping.quantity).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quantity Parsing', () => {
|
||||
test('should parse valid numbers', () => {
|
||||
expect(service.parseQuantity('100')).toBe(100);
|
||||
expect(service.parseQuantity('0')).toBe(0);
|
||||
expect(service.parseQuantity('50.7')).toBe(50); // Should floor to integer
|
||||
});
|
||||
|
||||
test('should handle formatted numbers', () => {
|
||||
expect(service.parseQuantity('1,000')).toBe(1000);
|
||||
expect(service.parseQuantity(' 250 ')).toBe(250);
|
||||
expect(service.parseQuantity('1 500')).toBe(1500);
|
||||
});
|
||||
|
||||
test('should handle empty or invalid values', () => {
|
||||
expect(service.parseQuantity('')).toBe(0);
|
||||
expect(service.parseQuantity(null)).toBe(0);
|
||||
expect(service.parseQuantity('abc')).toBe(null);
|
||||
expect(service.parseQuantity('N/A')).toBe(null);
|
||||
});
|
||||
|
||||
test('should ensure non-negative values', () => {
|
||||
expect(service.parseQuantity('-50')).toBe(0);
|
||||
expect(service.parseQuantity('-10.5')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cell Value Extraction', () => {
|
||||
test('should extract valid cell values', () => {
|
||||
const row = ['ABC123', 'Test Product', '50', 'Electronics'];
|
||||
|
||||
expect(service.getCellValue(row, 0)).toBe('ABC123');
|
||||
expect(service.getCellValue(row, 1)).toBe('Test Product');
|
||||
expect(service.getCellValue(row, 2)).toBe('50');
|
||||
expect(service.getCellValue(row, 3)).toBe('Electronics');
|
||||
});
|
||||
|
||||
test('should handle missing columns', () => {
|
||||
const row = ['ABC123', 'Test Product'];
|
||||
|
||||
expect(service.getCellValue(row, 5)).toBe('');
|
||||
expect(service.getCellValue(row, null)).toBe('');
|
||||
});
|
||||
|
||||
test('should trim whitespace', () => {
|
||||
const row = [' ABC123 ', ' Test Product '];
|
||||
|
||||
expect(service.getCellValue(row, 0)).toBe('ABC123');
|
||||
expect(service.getCellValue(row, 1)).toBe('Test Product');
|
||||
});
|
||||
|
||||
test('should handle null and undefined values', () => {
|
||||
const row = [null, undefined, '', 0];
|
||||
|
||||
expect(service.getCellValue(row, 0)).toBe('');
|
||||
expect(service.getCellValue(row, 1)).toBe('');
|
||||
expect(service.getCellValue(row, 2)).toBe('');
|
||||
expect(service.getCellValue(row, 3)).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Row Parsing', () => {
|
||||
test('should parse valid data rows', () => {
|
||||
const dataRows = [
|
||||
['ABC123', 'Test Product 1', '100', 'Electronics'],
|
||||
['DEF456', 'Test Product 2', '50', 'Books']
|
||||
];
|
||||
const columnMapping = { productCode: 0, description: 1, quantity: 2, category: 3 };
|
||||
const headerRow = ['Code', 'Name', 'Qty', 'Cat'];
|
||||
|
||||
const products = service.parseDataRows(dataRows, columnMapping, headerRow);
|
||||
|
||||
expect(products).toHaveLength(2);
|
||||
expect(products[0]).toMatchObject({
|
||||
rowNumber: 2,
|
||||
productCode: 'ABC123',
|
||||
description: 'Test Product 1',
|
||||
quantity: 100,
|
||||
category: 'Electronics',
|
||||
errors: []
|
||||
});
|
||||
});
|
||||
|
||||
test('should skip empty rows', () => {
|
||||
const dataRows = [
|
||||
['ABC123', 'Test Product 1', '100', 'Electronics'],
|
||||
['', '', '', ''],
|
||||
[null, null, null, null],
|
||||
['DEF456', 'Test Product 2', '50', 'Books']
|
||||
];
|
||||
const columnMapping = { productCode: 0, description: 1, quantity: 2, category: 3 };
|
||||
const headerRow = ['Code', 'Name', 'Qty', 'Cat'];
|
||||
|
||||
const products = service.parseDataRows(dataRows, columnMapping, headerRow);
|
||||
|
||||
expect(products).toHaveLength(2);
|
||||
expect(products[0].productCode).toBe('ABC123');
|
||||
expect(products[1].productCode).toBe('DEF456');
|
||||
});
|
||||
|
||||
test('should add errors for missing product codes', () => {
|
||||
const dataRows = [
|
||||
['', 'Test Product 1', '100', 'Electronics'],
|
||||
['DEF456', 'Test Product 2', '50', 'Books']
|
||||
];
|
||||
const columnMapping = { productCode: 0, description: 1, quantity: 2, category: 3 };
|
||||
const headerRow = ['Code', 'Name', 'Qty', 'Cat'];
|
||||
|
||||
const products = service.parseDataRows(dataRows, columnMapping, headerRow);
|
||||
|
||||
expect(products[0].errors).toHaveLength(1);
|
||||
expect(products[0].errors[0].type).toBe('MISSING_PRODUCT_CODE');
|
||||
expect(products[1].errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should add errors for invalid quantities', () => {
|
||||
const dataRows = [
|
||||
['ABC123', 'Test Product 1', 'invalid', 'Electronics'],
|
||||
['DEF456', 'Test Product 2', '50', 'Books']
|
||||
];
|
||||
const columnMapping = { productCode: 0, description: 1, quantity: 2, category: 3 };
|
||||
const headerRow = ['Code', 'Name', 'Qty', 'Cat'];
|
||||
|
||||
const products = service.parseDataRows(dataRows, columnMapping, headerRow);
|
||||
|
||||
expect(products[0].errors).toHaveLength(1);
|
||||
expect(products[0].errors[0].type).toBe('INVALID_QUANTITY');
|
||||
expect(products[1].errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Excel File Parsing', () => {
|
||||
test('should parse a simple Excel file', async () => {
|
||||
// Create a simple workbook for testing
|
||||
const testData = [
|
||||
['Product Code', 'Description', 'Quantity', 'Category'],
|
||||
['ABC123', 'Test Product 1', 100, 'Electronics'],
|
||||
['DEF456', 'Test Product 2', 50, 'Books']
|
||||
];
|
||||
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(testData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
const result = await service.parseExcelFile(buffer);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.products).toHaveLength(2);
|
||||
expect(result.data.totalRows).toBe(2);
|
||||
expect(result.data.columnMapping.productCode).toBe(0);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should handle empty Excel files', async () => {
|
||||
const worksheet = XLSX.utils.aoa_to_sheet([]);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
const result = await service.parseExcelFile(buffer);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors[0].type).toBe('PARSE_ERROR');
|
||||
expect(result.errors[0].message).toContain('empty');
|
||||
});
|
||||
|
||||
test('should handle invalid file buffers', async () => {
|
||||
const invalidBuffer = Buffer.from('not an excel file');
|
||||
|
||||
const result = await service.parseExcelFile(invalidBuffer);
|
||||
|
||||
// xlsx library is quite forgiving, so this might actually succeed with empty data
|
||||
// Let's check that it either fails or returns empty data
|
||||
if (!result.success) {
|
||||
expect(result.errors[0].type).toBe('PARSE_ERROR');
|
||||
} else {
|
||||
// If it succeeds, it should have empty or minimal data
|
||||
expect(result.data.products.length).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle missing sheet names', async () => {
|
||||
const testData = [['Header'], ['Data']];
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(testData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
const result = await service.parseExcelFile(buffer, { sheetName: 'NonExistent' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errors[0].message).toContain('Sheet "NonExistent" not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Column Suggestions', () => {
|
||||
test('should provide column suggestions', () => {
|
||||
const headers = ['Item_ID', 'Product_Name', 'Stock_Count', 'Product_Type'];
|
||||
const suggestions = service.getColumnSuggestions(headers);
|
||||
|
||||
expect(suggestions.productCode).toHaveLength(1);
|
||||
expect(suggestions.productCode[0].header).toBe('Item_ID');
|
||||
expect(suggestions.description).toHaveLength(1);
|
||||
expect(suggestions.description[0].header).toBe('Product_Name');
|
||||
expect(suggestions.quantity).toHaveLength(1);
|
||||
expect(suggestions.quantity[0].header).toBe('Stock_Count');
|
||||
expect(suggestions.category).toHaveLength(1);
|
||||
expect(suggestions.category[0].header).toBe('Product_Type');
|
||||
});
|
||||
|
||||
test('should rank suggestions by relevance', () => {
|
||||
const headers = ['Code', 'Product_Code', 'Item_Code_Number'];
|
||||
const suggestions = service.getColumnSuggestions(headers);
|
||||
|
||||
expect(suggestions.productCode).toHaveLength(3);
|
||||
// Should have suggestions sorted by score
|
||||
expect(suggestions.productCode[0].score).toBeGreaterThanOrEqual(suggestions.productCode[1].score);
|
||||
expect(suggestions.productCode[1].score).toBeGreaterThanOrEqual(suggestions.productCode[2].score);
|
||||
|
||||
// Both 'Code' and 'Product_Code' should be high-scoring matches
|
||||
const topSuggestions = suggestions.productCode.slice(0, 2).map(s => s.header);
|
||||
expect(topSuggestions).toContain('Code');
|
||||
expect(topSuggestions).toContain('Product_Code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Validation', () => {
|
||||
test('should validate clean data successfully', () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100, errors: [] },
|
||||
{ productCode: 'DEF456', description: 'Product 2', quantity: 50, errors: [] }
|
||||
];
|
||||
|
||||
const validation = service.validateParsedData(products);
|
||||
|
||||
expect(validation.isValid).toBe(true);
|
||||
expect(validation.statistics.validProducts).toBe(2);
|
||||
expect(validation.statistics.invalidProducts).toBe(0);
|
||||
expect(validation.duplicates).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should detect duplicate product codes', () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100, errors: [], rowNumber: 2 },
|
||||
{ productCode: 'ABC123', description: 'Product 2', quantity: 50, errors: [], rowNumber: 3 },
|
||||
{ productCode: 'DEF456', description: 'Product 3', quantity: 25, errors: [], rowNumber: 4 }
|
||||
];
|
||||
|
||||
const validation = service.validateParsedData(products);
|
||||
|
||||
expect(validation.statistics.duplicateProducts).toBe(1);
|
||||
expect(validation.warnings).toHaveLength(1);
|
||||
expect(validation.warnings[0].type).toBe('DUPLICATE_PRODUCT_CODES');
|
||||
expect(validation.duplicates).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should count invalid products', () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100, errors: [] },
|
||||
{ productCode: '', description: 'Product 2', quantity: 50, errors: [{ type: 'MISSING_PRODUCT_CODE' }] }
|
||||
];
|
||||
|
||||
const validation = service.validateParsedData(products);
|
||||
|
||||
expect(validation.isValid).toBe(false);
|
||||
expect(validation.statistics.validProducts).toBe(1);
|
||||
expect(validation.statistics.invalidProducts).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
test('should handle mixed data types in cells', async () => {
|
||||
const testData = [
|
||||
['Product Code', 'Description', 'Quantity'],
|
||||
[123, 'Product with numeric code', '50'],
|
||||
['ABC456', 789, 100], // Numeric description
|
||||
['DEF789', 'Normal Product', 'N/A'] // Invalid quantity
|
||||
];
|
||||
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(testData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
const result = await service.parseExcelFile(buffer);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.products).toHaveLength(3);
|
||||
expect(result.data.products[0].productCode).toBe('123');
|
||||
expect(result.data.products[1].description).toBe('789');
|
||||
expect(result.data.products[2].quantity).toBe(null);
|
||||
expect(result.data.products[2].errors).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should handle Excel files with multiple sheets', async () => {
|
||||
const testData1 = [['Code', 'Name'], ['ABC123', 'Product 1']];
|
||||
const testData2 = [['Product_Code', 'Description'], ['DEF456', 'Product 2']];
|
||||
|
||||
const worksheet1 = XLSX.utils.aoa_to_sheet(testData1);
|
||||
const worksheet2 = XLSX.utils.aoa_to_sheet(testData2);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet1, 'Inventory');
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet2, 'Products');
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
const result = await service.parseExcelFile(buffer, { sheetName: 'Products' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.sheetName).toBe('Products');
|
||||
expect(result.data.availableSheets).toEqual(['Inventory', 'Products']);
|
||||
expect(result.data.products[0].productCode).toBe('DEF456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data Validation and Error Handling', () => {
|
||||
describe('Product Code Validation', () => {
|
||||
test('should validate correct product codes', () => {
|
||||
expect(service.validateProductCode('ABC123')).toMatchObject({ isValid: true });
|
||||
expect(service.validateProductCode('ITEM-001')).toMatchObject({ isValid: true });
|
||||
expect(service.validateProductCode('SKU_456')).toMatchObject({ isValid: true });
|
||||
});
|
||||
|
||||
test('should reject invalid product codes', () => {
|
||||
expect(service.validateProductCode('')).toMatchObject({ isValid: false });
|
||||
expect(service.validateProductCode('A'.repeat(51))).toMatchObject({ isValid: false });
|
||||
expect(service.validateProductCode('ABC@123')).toMatchObject({ isValid: false });
|
||||
expect(service.validateProductCode('ABC 123')).toMatchObject({ isValid: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comprehensive Data Validation', () => {
|
||||
test('should validate clean data successfully', async () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100, category: 'Electronics', errors: [], rowNumber: 2 },
|
||||
{ productCode: 'DEF456', description: 'Product 2', quantity: 50, category: 'Books', errors: [], rowNumber: 3 }
|
||||
];
|
||||
|
||||
const validation = await service.validateImportData(products);
|
||||
|
||||
expect(validation.isValid).toBe(true);
|
||||
expect(validation.statistics.validProducts).toBe(2);
|
||||
expect(validation.statistics.invalidProducts).toBe(0);
|
||||
expect(validation.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should detect various validation errors', async () => {
|
||||
const products = [
|
||||
{ productCode: '', description: 'Product 1', quantity: 100, errors: [], rowNumber: 2 },
|
||||
{ productCode: 'ABC123', description: 'A'.repeat(501), quantity: -10, errors: [], rowNumber: 3 },
|
||||
{ productCode: 'DEF@456', description: 'Product 3', quantity: null, errors: [], rowNumber: 4 }
|
||||
];
|
||||
|
||||
const validation = await service.validateImportData(products);
|
||||
|
||||
expect(validation.isValid).toBe(false);
|
||||
expect(validation.statistics.validProducts).toBe(0);
|
||||
expect(validation.statistics.invalidProducts).toBe(3);
|
||||
|
||||
// Check specific error types
|
||||
const product1Errors = validation.validatedProducts[0].errors;
|
||||
expect(product1Errors.some(e => e.type === 'MISSING_PRODUCT_CODE')).toBe(true);
|
||||
|
||||
const product2Errors = validation.validatedProducts[1].errors;
|
||||
expect(product2Errors.some(e => e.type === 'DESCRIPTION_TOO_LONG')).toBe(true);
|
||||
expect(product2Errors.some(e => e.type === 'NEGATIVE_QUANTITY')).toBe(true);
|
||||
|
||||
const product3Errors = validation.validatedProducts[2].errors;
|
||||
expect(product3Errors.some(e => e.type === 'INVALID_PRODUCT_CODE_FORMAT')).toBe(true);
|
||||
expect(product3Errors.some(e => e.type === 'INVALID_QUANTITY')).toBe(true);
|
||||
});
|
||||
|
||||
test('should detect duplicate product codes', async () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100, errors: [], rowNumber: 2 },
|
||||
{ productCode: 'ABC123', description: 'Product 2', quantity: 50, errors: [], rowNumber: 3 },
|
||||
{ productCode: 'DEF456', description: 'Product 3', quantity: 25, errors: [], rowNumber: 4 }
|
||||
];
|
||||
|
||||
const validation = await service.validateImportData(products);
|
||||
|
||||
expect(validation.statistics.duplicateProducts).toBe(1);
|
||||
expect(validation.duplicates).toHaveLength(1);
|
||||
expect(validation.duplicates[0].productCode).toBe('ABC123');
|
||||
expect(validation.duplicates[0].rows).toEqual([2, 3]);
|
||||
expect(validation.warnings.some(w => w.type === 'DUPLICATE_PRODUCT_CODES')).toBe(true);
|
||||
});
|
||||
|
||||
test('should add warnings for large quantities', async () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 1500000, errors: [], rowNumber: 2 }
|
||||
];
|
||||
|
||||
const validation = await service.validateImportData(products);
|
||||
|
||||
expect(validation.isValid).toBe(true);
|
||||
expect(validation.validatedProducts[0].warnings.some(w => w.type === 'LARGE_QUANTITY')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Duplicate Handling', () => {
|
||||
test('should skip duplicates when strategy is skip', () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100 },
|
||||
{ productCode: 'ABC123', description: 'Product 2', quantity: 50 },
|
||||
{ productCode: 'DEF456', description: 'Product 3', quantity: 25 }
|
||||
];
|
||||
|
||||
const processed = service.handleDuplicates(products, 'skip');
|
||||
|
||||
expect(processed).toHaveLength(3);
|
||||
expect(processed[0].skipped).toBeUndefined();
|
||||
expect(processed[1].skipped).toBe(true);
|
||||
expect(processed[1].warnings.some(w => w.type === 'DUPLICATE_SKIPPED')).toBe(true);
|
||||
expect(processed[2].skipped).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should mark for update when strategy is update', () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100 },
|
||||
{ productCode: 'ABC123', description: 'Product 2', quantity: 50 },
|
||||
{ productCode: 'DEF456', description: 'Product 3', quantity: 25 }
|
||||
];
|
||||
|
||||
const processed = service.handleDuplicates(products, 'update');
|
||||
|
||||
expect(processed).toHaveLength(3);
|
||||
expect(processed[0].updateExisting).toBeUndefined();
|
||||
expect(processed[1].updateExisting).toBe(true);
|
||||
expect(processed[1].warnings.some(w => w.type === 'DUPLICATE_WILL_UPDATE')).toBe(true);
|
||||
expect(processed[2].updateExisting).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should rename duplicates when strategy is rename', () => {
|
||||
const products = [
|
||||
{ productCode: 'ABC123', description: 'Product 1', quantity: 100 },
|
||||
{ productCode: 'ABC123', description: 'Product 2', quantity: 50 },
|
||||
{ productCode: 'ABC123', description: 'Product 3', quantity: 25 }
|
||||
];
|
||||
|
||||
const processed = service.handleDuplicates(products, 'rename');
|
||||
|
||||
expect(processed).toHaveLength(3);
|
||||
expect(processed[0].productCode).toBe('ABC123');
|
||||
expect(processed[1].productCode).toBe('ABC123_2');
|
||||
expect(processed[1].originalProductCode).toBe('ABC123');
|
||||
expect(processed[2].productCode).toBe('ABC123_3');
|
||||
expect(processed[2].originalProductCode).toBe('ABC123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete Import Process', () => {
|
||||
test('should process complete import workflow', async () => {
|
||||
// Create test Excel file
|
||||
const testData = [
|
||||
['Product Code', 'Description', 'Quantity', 'Category'],
|
||||
['ABC123', 'Test Product 1', 100, 'Electronics'],
|
||||
['DEF456', 'Test Product 2', 50, 'Books']
|
||||
];
|
||||
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(testData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
const results = await service.processImport(buffer, {
|
||||
filename: 'test.xlsx',
|
||||
duplicateStrategy: 'skip',
|
||||
importToDatabase: false // Don't actually import to database in test
|
||||
});
|
||||
|
||||
expect(results.success).toBe(true);
|
||||
expect(results.parseResults.success).toBe(true);
|
||||
expect(results.validationResults.isValid).toBe(true);
|
||||
expect(results.parseResults.data.products).toHaveLength(2);
|
||||
expect(results.validationResults.statistics.validProducts).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle import process errors gracefully', async () => {
|
||||
const invalidBuffer = Buffer.from('invalid data');
|
||||
|
||||
const results = await service.processImport(invalidBuffer, {
|
||||
filename: 'invalid.xlsx'
|
||||
});
|
||||
|
||||
// Should handle the error gracefully
|
||||
expect(results.success).toBeDefined();
|
||||
expect(results.parseResults).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Reporting', () => {
|
||||
test('should provide detailed error information', async () => {
|
||||
const products = [
|
||||
{ productCode: '', description: 'Product 1', quantity: null, errors: [], rowNumber: 2 },
|
||||
{ productCode: 'ABC@123', description: 'A'.repeat(501), quantity: -10, errors: [], rowNumber: 3 }
|
||||
];
|
||||
|
||||
const validation = await service.validateImportData(products);
|
||||
|
||||
expect(validation.validatedProducts[0].errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'MISSING_PRODUCT_CODE',
|
||||
field: 'productCode',
|
||||
message: expect.any(String)
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'INVALID_QUANTITY',
|
||||
field: 'quantity',
|
||||
message: expect.any(String)
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
expect(validation.validatedProducts[1].errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'INVALID_PRODUCT_CODE_FORMAT',
|
||||
field: 'productCode'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'DESCRIPTION_TOO_LONG',
|
||||
field: 'description'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'NEGATIVE_QUANTITY',
|
||||
field: 'quantity'
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});});
|
||||
Reference in New Issue
Block a user