Initial commit: Inventory Barcode System

This commit is contained in:
2025-07-22 20:24:51 -04:00
commit 511b01748d
63 changed files with 26932 additions and 0 deletions

View File

@ -0,0 +1,606 @@
const XLSX = require('xlsx');
const fs = require('fs').promises;
const path = require('path');
// Mock dependencies before importing the service
jest.mock('../models/database', () => ({
getDatabase: jest.fn()
}));
jest.mock('fs', () => ({
promises: {
mkdir: jest.fn(),
writeFile: jest.fn(),
readdir: jest.fn(),
stat: jest.fn(),
unlink: jest.fn()
}
}));
const ExcelExportService = require('../services/ExcelExportService');
const database = require('../models/database');
describe('ExcelExportService', () => {
let exportService;
let mockDb;
beforeEach(() => {
exportService = new ExcelExportService();
// Mock database instance
mockDb = {
prepare: jest.fn(),
exec: jest.fn()
};
database.getDatabase = jest.fn().mockReturnValue(mockDb);
// Clear all mocks
jest.clearAllMocks();
});
describe('constructor', () => {
it('should initialize with correct export directory', () => {
expect(exportService.exportDirectory).toContain(path.join('data', 'exports'));
});
it('should ensure export directory exists', async () => {
await exportService.ensureExportDirectory();
expect(fs.mkdir).toHaveBeenCalledWith(
expect.stringContaining(path.join('data', 'exports')),
{ recursive: true }
);
});
});
describe('getInventoryData', () => {
beforeEach(() => {
const mockStmt = {
all: jest.fn().mockReturnValue([
{
product_code: 'TEST001',
description: 'Test Product 1',
category: 'Electronics',
unit_of_measure: 'pcs',
current_level: 100,
minimum_level: 10,
maximum_level: 500,
last_updated: '2024-01-15T10:30:00Z',
updated_by: 'user1',
stock_status: 'Normal'
},
{
product_code: 'TEST002',
description: 'Test Product 2',
category: 'Tools',
unit_of_measure: 'pcs',
current_level: 5,
minimum_level: 10,
maximum_level: 100,
last_updated: '2024-01-14T15:45:00Z',
updated_by: 'user2',
stock_status: 'Low Stock'
}
])
};
mockDb.prepare.mockReturnValue(mockStmt);
});
it('should retrieve inventory data without filters', async () => {
const data = await exportService.getInventoryData();
expect(data).toHaveLength(2);
expect(data[0].product_code).toBe('TEST001');
expect(data[1].stock_status).toBe('Low Stock');
});
it('should apply category filter', async () => {
await exportService.getInventoryData({ category: 'Electronics' });
const query = mockDb.prepare.mock.calls[0][0];
expect(query).toContain('WHERE p.category = ?');
});
it('should apply stock status filter', async () => {
await exportService.getInventoryData({ stockStatus: 'low' });
const query = mockDb.prepare.mock.calls[0][0];
expect(query).toContain('i.current_level <= i.minimum_level');
});
it('should apply updated since filter', async () => {
const since = '2024-01-01T00:00:00Z';
await exportService.getInventoryData({ updatedSince: since });
const query = mockDb.prepare.mock.calls[0][0];
expect(query).toContain('i.last_updated >= ?');
});
it('should apply product codes filter', async () => {
await exportService.getInventoryData({ productCodes: ['TEST001', 'TEST002'] });
const query = mockDb.prepare.mock.calls[0][0];
expect(query).toContain('p.product_code IN (?,?)');
});
});
describe('createNewExcelFile', () => {
const mockInventoryData = [
{
product_code: 'TEST001',
description: 'Test Product 1',
category: 'Electronics',
unit_of_measure: 'pcs',
current_level: 100,
minimum_level: 10,
maximum_level: 500,
last_updated: '2024-01-15T10:30:00Z',
updated_by: 'user1',
stock_status: 'Normal'
}
];
it('should create new Excel file with inventory sheet', async () => {
const result = await exportService.createNewExcelFile(mockInventoryData);
expect(result.workbook).toBeDefined();
expect(result.metadata.sheets).toContain('Inventory');
expect(result.metadata.sheets).toContain('Summary');
expect(result.metadata.recordCount).toBe(1);
});
it('should include history sheet when requested', async () => {
// Mock history data
const mockHistoryStmt = {
all: jest.fn().mockReturnValue([
{
product_code: 'TEST001',
description: 'Test Product 1',
old_level: 90,
new_level: 100,
change_reason: 'Stock adjustment',
updated_by: 'user1',
updated_at: '2024-01-15T10:30:00Z'
}
])
};
mockDb.prepare.mockReturnValue(mockHistoryStmt);
const result = await exportService.createNewExcelFile(mockInventoryData, { includeHistory: true });
expect(result.metadata.sheets).toContain('History');
expect(result.metadata.includeHistory).toBe(true);
});
it('should exclude audit info when requested', async () => {
const result = await exportService.createNewExcelFile(mockInventoryData, { includeAuditInfo: false });
expect(result.metadata.includeAuditInfo).toBe(false);
});
});
describe('createInventorySheet', () => {
const mockInventoryData = [
{
product_code: 'TEST001',
description: 'Test Product 1',
category: 'Electronics',
unit_of_measure: 'pcs',
current_level: 100,
minimum_level: 10,
maximum_level: 500,
last_updated: '2024-01-15T10:30:00Z',
updated_by: 'user1',
stock_status: 'Normal'
}
];
it('should create worksheet with correct headers', () => {
const worksheet = exportService.createInventorySheet(mockInventoryData, true);
// Convert worksheet to array to check content
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const headers = data[0];
expect(headers).toContain('Product Code');
expect(headers).toContain('Description');
expect(headers).toContain('Current Level');
expect(headers).toContain('Last Updated');
expect(headers).toContain('Updated By');
});
it('should exclude audit info when requested', () => {
const worksheet = exportService.createInventorySheet(mockInventoryData, false);
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const headers = data[0];
expect(headers).not.toContain('Last Updated');
expect(headers).not.toContain('Updated By');
});
it('should include data rows with correct values', () => {
const worksheet = exportService.createInventorySheet(mockInventoryData, true);
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const dataRow = data[1];
expect(dataRow[0]).toBe('TEST001'); // Product Code
expect(dataRow[1]).toBe('Test Product 1'); // Description
expect(dataRow[4]).toBe(100); // Current Level
});
it('should set column widths', () => {
const worksheet = exportService.createInventorySheet(mockInventoryData, true);
expect(worksheet['!cols']).toBeDefined();
expect(worksheet['!cols']).toHaveLength(10); // 8 base + 2 audit columns
});
});
describe('updateOriginalExcelFile', () => {
let originalFileBuffer;
beforeEach(() => {
// Create a mock Excel file buffer
const mockWorkbook = XLSX.utils.book_new();
const mockData = [
['Product Code', 'Description', 'Quantity'],
['TEST001', 'Old Description', 50],
['TEST002', 'Another Product', 25]
];
const mockWorksheet = XLSX.utils.aoa_to_sheet(mockData);
XLSX.utils.book_append_sheet(mockWorkbook, mockWorksheet, 'Sheet1');
originalFileBuffer = XLSX.write(mockWorkbook, { type: 'buffer', bookType: 'xlsx' });
});
it('should update existing products in original file', async () => {
const inventoryData = [
{
product_code: 'TEST001',
description: 'Updated Description',
current_level: 75,
category: 'Electronics',
last_updated: '2024-01-15T10:30:00Z'
}
];
const result = await exportService.updateOriginalExcelFile(originalFileBuffer, inventoryData);
expect(result.workbook).toBeDefined();
expect(result.metadata.updatedRows).toBe(1);
expect(result.metadata.preservedFormatting).toBe(true);
});
it('should add new products when includeNewProducts is true', async () => {
const inventoryData = [
{
product_code: 'TEST003',
description: 'New Product',
current_level: 30,
category: 'Tools'
}
];
const result = await exportService.updateOriginalExcelFile(
originalFileBuffer,
inventoryData,
{ includeNewProducts: true }
);
expect(result.metadata.addedRows).toBe(1);
});
it('should handle empty original file', async () => {
const emptyWorkbook = XLSX.utils.book_new();
const emptyWorksheet = XLSX.utils.aoa_to_sheet([]);
XLSX.utils.book_append_sheet(emptyWorkbook, emptyWorksheet, 'Sheet1');
const emptyBuffer = XLSX.write(emptyWorkbook, { type: 'buffer', bookType: 'xlsx' });
await expect(
exportService.updateOriginalExcelFile(emptyBuffer, [])
).rejects.toThrow('Original Excel file appears to be empty');
});
it('should handle missing sheet', async () => {
const inventoryData = [];
await expect(
exportService.updateOriginalExcelFile(originalFileBuffer, inventoryData, { sheetName: 'NonExistent' })
).rejects.toThrow('Sheet "NonExistent" not found');
});
});
describe('detectColumnMappings', () => {
it('should detect standard column mappings', () => {
const headerRow = ['Product Code', 'Description', 'Quantity', 'Category'];
const mapping = exportService.detectColumnMappings(headerRow);
expect(mapping.productCode).toBe(0);
expect(mapping.description).toBe(1);
expect(mapping.quantity).toBe(2);
expect(mapping.category).toBe(3);
});
it('should handle case-insensitive matching', () => {
const headerRow = ['PRODUCT_CODE', 'desc', 'QTY'];
const mapping = exportService.detectColumnMappings(headerRow);
expect(mapping.productCode).toBe(0);
expect(mapping.description).toBe(1);
expect(mapping.quantity).toBe(2);
});
it('should handle partial matches', () => {
const headerRow = ['Item Code', 'Product Name', 'Stock Level'];
const mapping = exportService.detectColumnMappings(headerRow);
expect(mapping.productCode).toBe(0);
expect(mapping.description).toBe(1);
expect(mapping.quantity).toBe(2);
});
it('should return null for unmatched columns', () => {
const headerRow = ['Unknown1', 'Unknown2'];
const mapping = exportService.detectColumnMappings(headerRow);
expect(mapping.productCode).toBeNull();
expect(mapping.description).toBeNull();
expect(mapping.quantity).toBeNull();
});
});
describe('exportInventoryToExcel', () => {
beforeEach(() => {
// Mock getInventoryData
jest.spyOn(exportService, 'getInventoryData').mockResolvedValue([
{
product_code: 'TEST001',
description: 'Test Product',
current_level: 100,
stock_status: 'Normal'
}
]);
// Mock createNewExcelFile
jest.spyOn(exportService, 'createNewExcelFile').mockResolvedValue({
workbook: XLSX.utils.book_new(),
metadata: { recordCount: 1 }
});
// Mock writeExcelFile
jest.spyOn(exportService, 'writeExcelFile').mockResolvedValue();
// Mock createExportSession
jest.spyOn(exportService, 'createExportSession').mockResolvedValue(123);
});
it('should export inventory successfully', async () => {
const result = await exportService.exportInventoryToExcel();
expect(result.success).toBe(true);
expect(result.filename).toMatch(/inventory_export_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.xlsx/);
expect(result.recordCount).toBe(1);
expect(result.sessionId).toBe(123);
});
it('should use custom filename when provided', async () => {
const customFilename = 'custom_export.xlsx';
const result = await exportService.exportInventoryToExcel({ filename: customFilename });
expect(result.filename).toBe(customFilename);
});
it('should handle export errors gracefully', async () => {
jest.spyOn(exportService, 'getInventoryData').mockRejectedValue(new Error('Database error'));
const result = await exportService.exportInventoryToExcel();
expect(result.success).toBe(false);
expect(result.error).toBe('Database error');
expect(result.filePath).toBeNull();
});
it('should preserve original formatting when buffer provided', async () => {
const mockBuffer = Buffer.from('mock excel data');
jest.spyOn(exportService, 'updateOriginalExcelFile').mockResolvedValue({
workbook: XLSX.utils.book_new(),
metadata: { preservedFormatting: true }
});
const result = await exportService.exportInventoryToExcel({
originalFileBuffer: mockBuffer,
preserveFormatting: true
});
expect(result.success).toBe(true);
expect(exportService.updateOriginalExcelFile).toHaveBeenCalledWith(
mockBuffer,
expect.any(Array),
expect.any(Object)
);
});
});
describe('generateExportFilename', () => {
it('should generate filename with timestamp', () => {
const filename = exportService.generateExportFilename();
expect(filename).toMatch(/inventory_export_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.xlsx/);
});
it('should use specified format', () => {
const filename = exportService.generateExportFilename('csv');
expect(filename.endsWith('.csv')).toBe(true);
});
});
describe('createExportSession', () => {
beforeEach(() => {
const mockStmt = {
run: jest.fn().mockReturnValue({ lastInsertRowid: 456 })
};
mockDb.prepare.mockReturnValue(mockStmt);
});
it('should create export session record', async () => {
const sessionData = {
filename: 'test_export.xlsx',
totalRecords: 100,
filters: { category: 'Electronics' },
includeHistory: true
};
const sessionId = await exportService.createExportSession(sessionData);
expect(sessionId).toBe(456);
expect(mockDb.exec).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS export_sessions'));
});
it('should handle database errors gracefully', async () => {
mockDb.prepare.mockImplementation(() => {
throw new Error('Database error');
});
const sessionId = await exportService.createExportSession({});
expect(sessionId).toBeNull();
});
});
describe('getExportHistory', () => {
beforeEach(() => {
const mockStmt = {
all: jest.fn().mockReturnValue([
{
id: 1,
filename: 'export1.xlsx',
total_records: 100,
export_date: '2024-01-15T10:30:00Z'
}
])
};
mockDb.prepare.mockReturnValue(mockStmt);
});
it('should retrieve export history', async () => {
const history = await exportService.getExportHistory();
expect(history).toHaveLength(1);
expect(history[0].filename).toBe('export1.xlsx');
});
it('should apply limit and offset', async () => {
await exportService.getExportHistory({ limit: 10, offset: 5 });
const mockStmt = mockDb.prepare.mock.results[0].value;
expect(mockStmt.all).toHaveBeenCalledWith(10, 5);
});
it('should handle database errors', async () => {
mockDb.prepare.mockImplementation(() => {
throw new Error('Database error');
});
const history = await exportService.getExportHistory();
expect(history).toEqual([]);
});
});
describe('cleanupOldExports', () => {
beforeEach(() => {
const oldDate = new Date(Date.now() - 48 * 60 * 60 * 1000); // 48 hours ago
const newDate = new Date(Date.now() - 12 * 60 * 60 * 1000); // 12 hours ago
fs.readdir.mockResolvedValue(['old_export.xlsx', 'new_export.xlsx']);
fs.stat
.mockResolvedValueOnce({ mtime: oldDate })
.mockResolvedValueOnce({ mtime: newDate });
fs.unlink.mockResolvedValue();
});
it('should delete old export files', async () => {
const result = await exportService.cleanupOldExports(24);
expect(result.success).toBe(true);
expect(result.deletedCount).toBe(1);
expect(fs.unlink).toHaveBeenCalledTimes(1);
});
it('should handle cleanup errors', async () => {
fs.readdir.mockRejectedValue(new Error('Directory error'));
const result = await exportService.cleanupOldExports();
expect(result.success).toBe(false);
expect(result.error).toBe('Directory error');
});
});
describe('writeExcelFile', () => {
it('should write Excel file to disk', async () => {
const mockWorkbook = XLSX.utils.book_new();
// Add a worksheet to make the workbook valid
const mockWorksheet = XLSX.utils.aoa_to_sheet([['Test']]);
XLSX.utils.book_append_sheet(mockWorkbook, mockWorksheet, 'Sheet1');
const filePath = '/test/path/export.xlsx';
await exportService.writeExcelFile(mockWorkbook, filePath, 'xlsx');
expect(fs.writeFile).toHaveBeenCalledWith(filePath, expect.any(Buffer));
});
it('should handle write errors', async () => {
fs.writeFile.mockRejectedValue(new Error('Write error'));
const mockWorkbook = XLSX.utils.book_new();
// Add a worksheet to make the workbook valid
const mockWorksheet = XLSX.utils.aoa_to_sheet([['Test']]);
XLSX.utils.book_append_sheet(mockWorkbook, mockWorksheet, 'Sheet1');
await expect(
exportService.writeExcelFile(mockWorkbook, '/test/path', 'xlsx')
).rejects.toThrow('Failed to write Excel file: Write error');
});
});
describe('getCellValue', () => {
it('should return cell value as string', () => {
const row = ['A', 'B', 'C'];
const value = exportService.getCellValue(row, 1);
expect(value).toBe('B');
});
it('should handle null column index', () => {
const row = ['A', 'B', 'C'];
const value = exportService.getCellValue(row, null);
expect(value).toBe('');
});
it('should handle out of bounds index', () => {
const row = ['A', 'B', 'C'];
const value = exportService.getCellValue(row, 5);
expect(value).toBe('');
});
it('should handle null/undefined values', () => {
const row = ['A', null, undefined, 'D'];
expect(exportService.getCellValue(row, 1)).toBe('');
expect(exportService.getCellValue(row, 2)).toBe('');
expect(exportService.getCellValue(row, 3)).toBe('D');
});
it('should trim whitespace', () => {
const row = [' A ', ' B '];
expect(exportService.getCellValue(row, 0)).toBe('A');
expect(exportService.getCellValue(row, 1)).toBe('B');
});
});
});