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,717 @@
const request = require('supertest');
const app = require('../server');
const Inventory = require('../models/Inventory');
const Product = require('../models/Product');
const database = require('../models/database');
// Mock the database and models
jest.mock('../models/database');
jest.mock('../models/Inventory');
jest.mock('../models/Product');
describe('Inventory API Endpoints', () => {
let mockDb;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Create mock database instance
mockDb = {
prepare: jest.fn(),
transaction: jest.fn()
};
database.getDatabase.mockReturnValue(mockDb);
database.getInstance = jest.fn().mockReturnValue(mockDb);
database.executeTransaction = jest.fn();
});
describe('GET /api/inventory', () => {
it('should return inventory summary for all products', async () => {
const mockInventorySummary = [
{
id: 1,
product_code: 'P001',
description: 'Product 1',
current_level: 10,
minimum_level: 5,
stock_status: 'normal'
},
{
id: 2,
product_code: 'P002',
description: 'Product 2',
current_level: 2,
minimum_level: 5,
stock_status: 'low'
}
];
Inventory.getInventorySummary.mockResolvedValue(mockInventorySummary);
const response = await request(app)
.get('/api/inventory')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveLength(2);
expect(response.body.count).toBe(2);
expect(Inventory.getInventorySummary).toHaveBeenCalledWith({});
});
it('should filter inventory by category', async () => {
const mockInventorySummary = [
{
id: 1,
product_code: 'P001',
description: 'Product 1',
category: 'Electronics',
current_level: 10
}
];
Inventory.getInventorySummary.mockResolvedValue(mockInventorySummary);
const response = await request(app)
.get('/api/inventory?category=Electronics')
.expect(200);
expect(response.body.success).toBe(true);
expect(Inventory.getInventorySummary).toHaveBeenCalledWith({ category: 'Electronics' });
});
it('should filter for low stock items', async () => {
const mockLowStockSummary = [
{
id: 2,
product_code: 'P002',
description: 'Product 2',
current_level: 2,
minimum_level: 5,
stock_status: 'low'
}
];
Inventory.getInventorySummary.mockResolvedValue(mockLowStockSummary);
const response = await request(app)
.get('/api/inventory?lowStock=true')
.expect(200);
expect(response.body.success).toBe(true);
expect(Inventory.getInventorySummary).toHaveBeenCalledWith({ lowStock: true });
});
it('should handle database errors gracefully', async () => {
Inventory.getInventorySummary.mockRejectedValue(new Error('Database connection failed'));
const response = await request(app)
.get('/api/inventory')
.expect(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to retrieve inventory summary');
});
});
describe('GET /api/inventory/low-stock', () => {
it('should return low stock items', async () => {
const mockLowStockItems = [
{
id: 1,
product_code: 'P001',
description: 'Low Stock Product',
current_level: 2,
minimum_level: 10
}
];
Inventory.getLowStockItems.mockResolvedValue(mockLowStockItems);
const response = await request(app)
.get('/api/inventory/low-stock')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveLength(1);
expect(response.body.count).toBe(1);
});
it('should handle errors when retrieving low stock items', async () => {
Inventory.getLowStockItems.mockRejectedValue(new Error('Database error'));
const response = await request(app)
.get('/api/inventory/low-stock')
.expect(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to retrieve low stock items');
});
});
describe('GET /api/inventory/product/:productId', () => {
it('should return inventory details for a specific product', async () => {
const mockInventory = {
id: 1,
product_id: 1,
current_level: 15,
minimum_level: 5,
maximum_level: 50,
toJSON: jest.fn().mockReturnValue({
id: 1,
product_id: 1,
current_level: 15,
minimum_level: 5,
maximum_level: 50
})
};
Inventory.getByProductId.mockResolvedValue(mockInventory);
const response = await request(app)
.get('/api/inventory/product/1')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.product_id).toBe(1);
expect(response.body.data.current_level).toBe(15);
});
it('should return 404 for non-existent inventory', async () => {
Inventory.getByProductId.mockResolvedValue(null);
const response = await request(app)
.get('/api/inventory/product/999')
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Inventory not found');
});
it('should return 400 for invalid product ID', async () => {
const response = await request(app)
.get('/api/inventory/product/invalid')
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid product ID');
});
});
describe('GET /api/inventory/product/:productId/level', () => {
it('should return current inventory level', async () => {
Inventory.getCurrentLevel.mockResolvedValue(25);
const response = await request(app)
.get('/api/inventory/product/1/level')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.product_id).toBe(1);
expect(response.body.data.current_level).toBe(25);
});
it('should return 400 for invalid product ID', async () => {
const response = await request(app)
.get('/api/inventory/product/invalid/level')
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid product ID');
});
});
describe('GET /api/inventory/product/:productId/history', () => {
it('should return inventory history with default pagination', async () => {
const mockHistory = [
{
id: 1,
product_id: 1,
old_level: 10,
new_level: 15,
change_reason: 'Stock received',
updated_at: '2023-01-01T10:00:00Z'
}
];
Inventory.getInventoryHistory.mockResolvedValue(mockHistory);
const response = await request(app)
.get('/api/inventory/product/1/history')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveLength(1);
expect(response.body.pagination.limit).toBe(50);
expect(response.body.pagination.offset).toBe(0);
expect(Inventory.getInventoryHistory).toHaveBeenCalledWith(1, {
limit: 50,
offset: 0,
startDate: undefined,
endDate: undefined
});
});
it('should return inventory history with custom pagination', async () => {
const mockHistory = [];
Inventory.getInventoryHistory.mockResolvedValue(mockHistory);
const response = await request(app)
.get('/api/inventory/product/1/history?limit=10&offset=20')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.pagination.limit).toBe(10);
expect(response.body.pagination.offset).toBe(20);
expect(Inventory.getInventoryHistory).toHaveBeenCalledWith(1, {
limit: 10,
offset: 20,
startDate: undefined,
endDate: undefined
});
});
it('should return 400 for invalid limit', async () => {
const response = await request(app)
.get('/api/inventory/product/1/history?limit=2000')
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid limit');
});
it('should return 400 for negative offset', async () => {
const response = await request(app)
.get('/api/inventory/product/1/history?offset=-1')
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid offset');
});
});
describe('PUT /api/inventory/product/:productId/level', () => {
it('should update inventory level successfully', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockUpdatedInventory = {
id: 1,
product_id: 1,
current_level: 20,
toJSON: jest.fn().mockReturnValue({
id: 1,
product_id: 1,
current_level: 20
})
};
Product.findById.mockResolvedValue(mockProduct);
Inventory.updateInventoryLevel.mockResolvedValue(mockUpdatedInventory);
const response = await request(app)
.put('/api/inventory/product/1/level')
.send({
newLevel: 20,
changeReason: 'Stock adjustment',
updatedBy: 'test-user'
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.current_level).toBe(20);
expect(response.body.message).toBe('Inventory level updated successfully');
expect(Inventory.updateInventoryLevel).toHaveBeenCalledWith(
1,
20,
'Stock adjustment',
'test-user'
);
});
it('should return 400 for invalid new level', async () => {
const response = await request(app)
.put('/api/inventory/product/1/level')
.send({ newLevel: 'invalid' })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid new level');
});
it('should return 400 for negative new level', async () => {
const response = await request(app)
.put('/api/inventory/product/1/level')
.send({ newLevel: -5 })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid new level');
});
it('should return 404 for non-existent product', async () => {
Product.findById.mockResolvedValue(null);
const response = await request(app)
.put('/api/inventory/product/999/level')
.send({ newLevel: 10 })
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Product not found');
});
it('should return 409 for concurrent update conflict', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
Product.findById.mockResolvedValue(mockProduct);
Inventory.updateInventoryLevel.mockRejectedValue(
new Error('Concurrent update detected. Please refresh and try again.')
);
const response = await request(app)
.put('/api/inventory/product/1/level')
.send({ newLevel: 10 })
.expect(409);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Concurrent update conflict');
});
});
describe('PUT /api/inventory/product/:productId', () => {
it('should update inventory settings successfully', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockInventory = {
id: 1,
product_id: 1,
current_level: 15,
minimum_level: 5,
maximum_level: 50,
version: 1,
validate: jest.fn().mockReturnValue({ isValid: true, errors: [] }),
save: jest.fn().mockResolvedValue(true),
toJSON: jest.fn().mockReturnValue({
id: 1,
product_id: 1,
current_level: 15,
minimum_level: 10,
maximum_level: 100
})
};
Product.findById.mockResolvedValue(mockProduct);
Inventory.getByProductId.mockResolvedValue(mockInventory);
const response = await request(app)
.put('/api/inventory/product/1')
.send({
minimum_level: 10,
maximum_level: 100,
updatedBy: 'test-user'
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toBe('Inventory settings updated successfully');
expect(mockInventory.minimum_level).toBe(10);
expect(mockInventory.maximum_level).toBe(100);
expect(mockInventory.save).toHaveBeenCalled();
});
it('should return 404 for non-existent inventory', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
Product.findById.mockResolvedValue(mockProduct);
Inventory.getByProductId.mockResolvedValue(null);
const response = await request(app)
.put('/api/inventory/product/1')
.send({ minimum_level: 10 })
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Inventory not found');
});
it('should return 400 for invalid minimum level', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockInventory = { id: 1, product_id: 1 };
Product.findById.mockResolvedValue(mockProduct);
Inventory.getByProductId.mockResolvedValue(mockInventory);
const response = await request(app)
.put('/api/inventory/product/1')
.send({ minimum_level: -5 })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid minimum level');
});
});
describe('POST /api/inventory/product/:productId', () => {
it('should create inventory record successfully', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockInventory = {
id: 1,
product_id: 1,
current_level: 10,
toJSON: jest.fn().mockReturnValue({
id: 1,
product_id: 1,
current_level: 10
})
};
Product.findById.mockResolvedValue(mockProduct);
Inventory.getByProductId.mockResolvedValue(null); // No existing inventory
Inventory.createForProduct.mockResolvedValue(mockInventory);
const response = await request(app)
.post('/api/inventory/product/1')
.send({
initialLevel: 10,
minimumLevel: 5,
maximumLevel: 50,
updatedBy: 'test-user'
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.message).toBe('Inventory record created successfully');
expect(Inventory.createForProduct).toHaveBeenCalledWith(1, 10, 5, 50, 'test-user');
});
it('should return 409 if inventory already exists', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockExistingInventory = { id: 1, product_id: 1 };
Product.findById.mockResolvedValue(mockProduct);
Inventory.getByProductId.mockResolvedValue(mockExistingInventory);
const response = await request(app)
.post('/api/inventory/product/1')
.send({ initialLevel: 10 })
.expect(409);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Inventory already exists');
});
it('should return 400 for invalid initial level', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
Product.findById.mockResolvedValue(mockProduct);
Inventory.getByProductId.mockResolvedValue(null);
const response = await request(app)
.post('/api/inventory/product/1')
.send({ initialLevel: -5 })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid initial level');
});
});
describe('POST /api/inventory/bulk-update', () => {
it('should bulk update inventory levels successfully', async () => {
const updates = [
{ productId: 1, newLevel: 20, changeReason: 'Restock', updatedBy: 'user1' },
{ productId: 2, newLevel: 15, changeReason: 'Adjustment', updatedBy: 'user1' }
];
const mockUpdatedInventories = [
{
id: 1,
product_id: 1,
current_level: 20,
toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 20 })
},
{
id: 2,
product_id: 2,
current_level: 15,
toJSON: jest.fn().mockReturnValue({ id: 2, product_id: 2, current_level: 15 })
}
];
Inventory.bulkUpdateInventory.mockResolvedValue(mockUpdatedInventories);
const response = await request(app)
.post('/api/inventory/bulk-update')
.send({ updates })
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.count).toBe(2);
expect(response.body.message).toBe('Successfully updated 2 inventory records');
expect(Inventory.bulkUpdateInventory).toHaveBeenCalledWith(updates);
});
it('should return 400 for empty updates array', async () => {
const response = await request(app)
.post('/api/inventory/bulk-update')
.send({ updates: [] })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid input');
});
it('should return 400 for invalid update data', async () => {
const updates = [
{ productId: 'invalid', newLevel: 20 }
];
const response = await request(app)
.post('/api/inventory/bulk-update')
.send({ updates })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid update data');
});
it('should return 409 for concurrent update conflict', async () => {
const updates = [{ productId: 1, newLevel: 20 }];
Inventory.bulkUpdateInventory.mockRejectedValue(
new Error('Concurrent update detected for product 1. Please refresh and try again.')
);
const response = await request(app)
.post('/api/inventory/bulk-update')
.send({ updates })
.expect(409);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Concurrent update conflict');
});
});
describe('POST /api/inventory/adjust/:productId', () => {
it('should adjust inventory level positively', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockUpdatedInventory = {
id: 1,
product_id: 1,
current_level: 25,
toJSON: jest.fn().mockReturnValue({
id: 1,
product_id: 1,
current_level: 25
})
};
Product.findById.mockResolvedValue(mockProduct);
Inventory.getCurrentLevel.mockResolvedValue(20);
Inventory.updateInventoryLevel.mockResolvedValue(mockUpdatedInventory);
const response = await request(app)
.post('/api/inventory/adjust/1')
.send({
adjustment: 5,
changeReason: 'Stock received',
updatedBy: 'test-user'
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.current_level).toBe(25);
expect(response.body.data.adjustment).toBe(5);
expect(response.body.data.previous_level).toBe(20);
expect(Inventory.updateInventoryLevel).toHaveBeenCalledWith(
1,
25,
'Stock received',
'test-user'
);
});
it('should adjust inventory level negatively', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
const mockUpdatedInventory = {
id: 1,
product_id: 1,
current_level: 15,
toJSON: jest.fn().mockReturnValue({
id: 1,
product_id: 1,
current_level: 15
})
};
Product.findById.mockResolvedValue(mockProduct);
Inventory.getCurrentLevel.mockResolvedValue(20);
Inventory.updateInventoryLevel.mockResolvedValue(mockUpdatedInventory);
const response = await request(app)
.post('/api/inventory/adjust/1')
.send({ adjustment: -5 })
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.current_level).toBe(15);
expect(response.body.data.adjustment).toBe(-5);
expect(Inventory.updateInventoryLevel).toHaveBeenCalledWith(
1,
15,
'Inventory adjustment: -5',
'api-user'
);
});
it('should return 400 for adjustment that would cause negative inventory', async () => {
const mockProduct = { id: 1, name: 'Test Product' };
Product.findById.mockResolvedValue(mockProduct);
Inventory.getCurrentLevel.mockResolvedValue(5);
const response = await request(app)
.post('/api/inventory/adjust/1')
.send({ adjustment: -10 })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid adjustment');
expect(response.body.message).toContain('would result in negative inventory');
});
it('should return 400 for invalid adjustment type', async () => {
const response = await request(app)
.post('/api/inventory/adjust/1')
.send({ adjustment: 'invalid' })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid adjustment');
});
});
describe('Error Handling', () => {
it('should handle database connection errors', async () => {
Inventory.getInventorySummary.mockRejectedValue(new Error('Database connection failed'));
const response = await request(app)
.get('/api/inventory')
.expect(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to retrieve inventory summary');
});
it('should handle inventory not found errors', async () => {
Inventory.updateInventoryLevel.mockRejectedValue(
new Error('Inventory record for product ID 999 not found')
);
const mockProduct = { id: 999, name: 'Test Product' };
Product.findById.mockResolvedValue(mockProduct);
const response = await request(app)
.put('/api/inventory/product/999/level')
.send({ newLevel: 10 })
.expect(404);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Inventory not found');
});
});
});