Files
inventory-barcode-system/routes/products.js

776 lines
20 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const Product = require('../models/Product');
const ExcelImportService = require('../services/ExcelImportService');
const logger = require('../utils/logger');
const {
ValidationError,
NotFoundError,
ConflictError,
asyncHandler
} = require('../middleware/errorHandler');
const router = express.Router();
/**
* GET /api/products/test
* Test endpoint to verify API is working - MUST BE FIRST to avoid /:id conflict
*/
router.get('/test', (req, res) => {
console.log('Test endpoint accessed from:', req.ip, req.get('User-Agent'));
res.json({
success: true,
message: 'Products API is working',
timestamp: new Date().toISOString(),
server: {
nodeVersion: process.version,
platform: process.platform,
uptime: process.uptime()
},
request: {
method: req.method,
url: req.url,
baseUrl: req.baseUrl,
originalUrl: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent')
},
endpoints: [
'GET /api/products',
'GET /api/products/:id',
'POST /api/products',
'PUT /api/products/:id',
'DELETE /api/products/:id',
'POST /api/products/import/excel/preview',
'POST /api/products/import/excel',
'POST /api/products/bulk'
]
});
});
// Configure multer for file uploads
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
fileFilter: (req, file, cb) => {
// Accept Excel files
const allowedMimes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel', // .xls
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Only Excel files (.xlsx, .xls) are allowed'), false);
}
}
});
/**
* GET /api/products
* Get all products with optional filtering
*/
router.get('/', async (req, res, next) => {
try {
const startTime = Date.now();
logger.info('Retrieving products', {
requestId: req.requestId,
filters: req.query
});
const filters = {};
// Extract and validate query parameters for filtering
if (req.query.category) {
if (typeof req.query.category !== 'string' || req.query.category.length > 100) {
throw new ValidationError('Invalid category filter');
}
filters.category = req.query.category.trim();
}
if (req.query.name) {
if (typeof req.query.name !== 'string' || req.query.name.length > 200) {
throw new ValidationError('Invalid name filter');
}
filters.name = req.query.name.trim();
}
const products = await Product.findAll(filters);
const duration = Date.now() - startTime;
logger.info('Products retrieved successfully', {
requestId: req.requestId,
count: products.length,
duration: `${duration}ms`,
filters
});
res.json({
success: true,
data: products.map(product => product.toJSON()),
count: products.length,
filters: Object.keys(filters).length > 0 ? filters : undefined
});
} catch (error) {
logger.logError(error, {
operation: 'get_products',
requestId: req.requestId,
filters: req.query
});
next(error);
}
});
/**
* GET /api/products/api-test
* Test endpoint to verify API is working
*/
router.get('/api-test', (req, res) => {
console.log('Test endpoint accessed from:', req.ip, req.get('User-Agent'));
res.json({
success: true,
message: 'Products API is working',
timestamp: new Date().toISOString(),
server: {
nodeVersion: process.version,
platform: process.platform,
uptime: process.uptime()
},
request: {
method: req.method,
url: req.url,
baseUrl: req.baseUrl,
originalUrl: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent')
},
endpoints: [
'GET /api/products',
'GET /api/products/:id',
'POST /api/products',
'PUT /api/products/:id',
'DELETE /api/products/:id',
'POST /api/products/import/excel/preview',
'POST /api/products/import/excel',
'POST /api/products/bulk'
]
});
});
/**
* GET /api/products/:id
* Get a specific product by ID
*/
router.get('/:id', async (req, res) => {
try {
const productId = parseInt(req.params.id);
if (isNaN(productId)) {
return res.status(400).json({
success: false,
error: 'Invalid product ID',
message: 'Product ID must be a valid number'
});
}
const product = await Product.findById(productId);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found',
message: `Product with ID ${productId} does not exist`
});
}
res.json({
success: true,
data: product.toJSON()
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to retrieve product',
message: error.message
});
}
});
/**
* GET /api/products/barcode/:barcode
* Get a product by barcode
*/
router.get('/barcode/:barcode', async (req, res) => {
try {
const barcode = decodeURIComponent(req.params.barcode);
if (!barcode || barcode.trim() === '' || barcode.trim() === ' ') {
return res.status(400).json({
success: false,
error: 'Invalid barcode',
message: 'Barcode cannot be empty'
});
}
const product = await Product.findByBarcode(barcode);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found',
message: `Product with barcode ${barcode} does not exist`
});
}
res.json({
success: true,
data: product.toJSON()
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to retrieve product',
message: error.message
});
}
});
/**
* POST /api/products
* Create a new product
*/
router.post('/', async (req, res) => {
try {
const productData = req.body;
// Create new product instance
const product = new Product(productData);
// Validate the product
const validation = product.validate();
if (!validation.isValid) {
return res.status(400).json({
success: false,
error: 'Validation failed',
message: 'Product data is invalid',
details: validation.errors
});
}
// Save the product
await product.save();
res.status(201).json({
success: true,
data: product.toJSON(),
message: 'Product created successfully'
});
} catch (error) {
if (error.message.includes('already exists')) {
res.status(409).json({
success: false,
error: 'Conflict',
message: error.message
});
} else {
res.status(500).json({
success: false,
error: 'Failed to create product',
message: error.message
});
}
}
});
/**
* PUT /api/products/:id
* Update an existing product
*/
router.put('/:id', async (req, res) => {
try {
const productId = parseInt(req.params.id);
if (isNaN(productId)) {
return res.status(400).json({
success: false,
error: 'Invalid product ID',
message: 'Product ID must be a valid number'
});
}
// Find existing product
const existingProduct = await Product.findById(productId);
if (!existingProduct) {
return res.status(404).json({
success: false,
error: 'Product not found',
message: `Product with ID ${productId} does not exist`
});
}
// Update product data
const updatedData = { ...existingProduct.toJSON(), ...req.body, id: productId };
const product = new Product(updatedData);
// Validate the updated product
const validation = product.validate();
if (!validation.isValid) {
return res.status(400).json({
success: false,
error: 'Validation failed',
message: 'Updated product data is invalid',
details: validation.errors
});
}
// Save the updated product
await product.save();
res.json({
success: true,
data: product.toJSON(),
message: 'Product updated successfully'
});
} catch (error) {
if (error.message.includes('already exists')) {
res.status(409).json({
success: false,
error: 'Conflict',
message: error.message
});
} else {
res.status(500).json({
success: false,
error: 'Failed to update product',
message: error.message
});
}
}
});
/**
* DELETE /api/products/:id
* Delete a product
*/
router.delete('/:id', async (req, res) => {
try {
const productId = parseInt(req.params.id);
if (isNaN(productId)) {
return res.status(400).json({
success: false,
error: 'Invalid product ID',
message: 'Product ID must be a valid number'
});
}
const deleted = await Product.deleteById(productId);
if (!deleted) {
return res.status(404).json({
success: false,
error: 'Product not found',
message: `Product with ID ${productId} does not exist`
});
}
res.json({
success: true,
message: 'Product deleted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to delete product',
message: error.message
});
}
});
/**
* POST /api/products/import/excel
* Import products from Excel file
*/
router.post('/import/excel', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: 'No file uploaded',
message: 'Please upload an Excel file'
});
}
const options = {
filename: req.file.originalname,
checkExisting: req.body.checkExisting !== 'false',
duplicateStrategy: req.body.duplicateStrategy || 'skip',
importToDatabase: req.body.importToDatabase !== 'false',
forceImport: req.body.forceImport === 'true',
updatedBy: req.body.updatedBy || 'api-user'
};
const excelImportService = new ExcelImportService();
const results = await excelImportService.processImport(req.file.buffer, options);
if (!results.success) {
return res.status(400).json({
success: false,
error: 'Import failed',
message: 'Failed to process Excel file',
details: results.errors
});
}
// Determine response status based on validation results
let statusCode = 200;
if (results.validationResults && !results.validationResults.isValid) {
statusCode = 422; // Unprocessable Entity
}
res.status(statusCode).json({
success: true,
data: {
parseResults: results.parseResults,
validationResults: results.validationResults,
importResults: results.importResults
},
message: 'Excel file processed successfully'
});
} catch (error) {
if (error.message.includes('Only Excel files')) {
res.status(400).json({
success: false,
error: 'Invalid file type',
message: error.message
});
} else {
res.status(500).json({
success: false,
error: 'Import failed',
message: error.message
});
}
}
});
/**
* POST /api/products/import/excel/preview
* Preview Excel file import without saving to database
*/
/**
* Direct import preview endpoint that doesn't rely on ExcelImportService
* This is a simplified version for testing
*/
router.post('/direct-import-preview', upload.single('file'), async (req, res) => {
console.log('Direct import preview endpoint hit:', {
hasFile: !!req.file,
filename: req.file?.originalname,
size: req.file?.size,
mimetype: req.file?.mimetype
});
try {
if (!req.file) {
console.log('No file uploaded');
return res.status(400).json({
success: false,
error: 'No file uploaded',
message: 'Please upload an Excel file'
});
}
console.log('Processing file:', req.file.originalname);
// Simple mock response for testing
const mockResponse = {
success: true,
data: {
preview: {
totalRows: 3,
validProducts: 2,
invalidProducts: 1,
duplicateProducts: 0,
existingProducts: 0,
sampleProducts: [
{ product_code: 'TEST001', description: 'Test Product 1', quantity: 10, isValid: true },
{ product_code: 'TEST002', description: 'Test Product 2', quantity: 20, isValid: true },
{ product_code: '', description: 'Invalid Product', quantity: 0, isValid: false, validationErrors: [{ message: 'Missing product code' }] }
]
},
validationResults: {
isValid: false,
errors: [
{ row: 3, message: 'Missing product code' }
],
statistics: {
validProducts: 2,
invalidProducts: 1,
duplicateProducts: 0,
existingProducts: 0
}
}
},
message: 'Excel file preview generated successfully (direct endpoint)'
};
console.log('Sending direct import response');
res.json(mockResponse);
} catch (error) {
console.error('Direct import preview error:', error);
res.status(500).json({
success: false,
error: 'Import preview failed',
message: error.message
});
}
});
/**
* Regular import preview endpoint
*/
router.post('/import/excel/preview', upload.single('file'), async (req, res) => {
console.log('Import preview endpoint hit:', {
hasFile: !!req.file,
filename: req.file?.originalname,
size: req.file?.size,
mimetype: req.file?.mimetype
});
try {
if (!req.file) {
console.log('No file uploaded');
return res.status(400).json({
success: false,
error: 'No file uploaded',
message: 'Please upload an Excel file'
});
}
console.log('Processing file:', req.file.originalname);
// For now, return a simple mock response to test the endpoint
const mockResponse = {
success: true,
data: {
preview: {
totalRows: 3,
validProducts: 2,
invalidProducts: 1,
duplicateProducts: 0,
existingProducts: 0,
sampleProducts: [
{ product_code: 'TEST001', description: 'Test Product 1', quantity: 10, isValid: true },
{ product_code: 'TEST002', description: 'Test Product 2', quantity: 20, isValid: true },
{ product_code: '', description: 'Invalid Product', quantity: 0, isValid: false, validationErrors: [{ message: 'Missing product code' }] }
]
},
validationResults: {
isValid: false,
errors: [
{ row: 3, message: 'Missing product code' }
],
statistics: {
validProducts: 2,
invalidProducts: 1,
duplicateProducts: 0,
existingProducts: 0
}
}
},
message: 'Excel file preview generated successfully (mock data)'
};
console.log('Sending mock response');
res.json(mockResponse);
/* Original code - commented out for testing
const options = {
filename: req.file.originalname,
checkExisting: req.body.checkExisting !== 'false',
importToDatabase: false // Never import for preview
};
const excelImportService = new ExcelImportService();
const results = await excelImportService.processImport(req.file.buffer, options);
if (!results.success) {
return res.status(400).json({
success: false,
error: 'Preview failed',
message: 'Failed to process Excel file',
details: results.errors
});
}
res.json({
success: true,
data: {
parseResults: results.parseResults,
validationResults: results.validationResults,
preview: {
totalRows: results.parseResults.data.totalRows,
validProducts: results.validationResults.statistics.validProducts,
invalidProducts: results.validationResults.statistics.invalidProducts,
duplicateProducts: results.validationResults.statistics.duplicateProducts,
existingProducts: results.validationResults.statistics.existingProducts,
sampleProducts: results.validationResults.validatedProducts.slice(0, 5) // First 5 for preview
}
},
message: 'Excel file preview generated successfully'
});
*/
} catch (error) {
console.error('Import preview error:', error);
if (error.message.includes('Only Excel files')) {
res.status(400).json({
success: false,
error: 'Invalid file type',
message: error.message
});
} else {
res.status(500).json({
success: false,
error: 'Preview failed',
message: error.message
});
}
}
});
/**
* POST /api/products/bulk
* Create multiple products in bulk
*/
router.post('/bulk', async (req, res) => {
try {
const { products } = req.body;
if (!Array.isArray(products) || products.length === 0) {
return res.status(400).json({
success: false,
error: 'Invalid input',
message: 'Products array is required and cannot be empty'
});
}
const results = {
success: true,
created: 0,
failed: 0,
errors: [],
createdProducts: []
};
// Process each product
for (let i = 0; i < products.length; i++) {
try {
const productData = products[i];
const product = new Product(productData);
// Validate the product
const validation = product.validate();
if (!validation.isValid) {
results.failed++;
results.errors.push({
index: i,
productData: productData,
errors: validation.errors
});
continue;
}
// Save the product
await product.save();
results.created++;
results.createdProducts.push(product.toJSON());
} catch (error) {
results.failed++;
results.errors.push({
index: i,
productData: products[i],
error: error.message
});
}
}
// Determine response status
let statusCode = 200;
if (results.failed > 0 && results.created === 0) {
statusCode = 400; // All failed
results.success = false;
} else if (results.failed > 0) {
statusCode = 207; // Partial success
} else {
statusCode = 201; // All created
}
res.status(statusCode).json({
...results,
message: `Bulk operation completed: ${results.created} created, ${results.failed} failed`
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Bulk operation failed',
message: error.message
});
}
});
/**
* GET /api/products/categories
* Get all unique product categories
*/
router.get('/categories', async (req, res) => {
try {
const categories = await Product.getCategories();
res.json(categories);
} catch (error) {
console.error('Error fetching categories:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch categories',
message: error.message
});
}
});
/**
* Catch-all route for debugging
*/
router.all('*', (req, res) => {
console.log('Unmatched products route:', {
method: req.method,
url: req.url,
originalUrl: req.originalUrl,
baseUrl: req.baseUrl
});
res.status(404).json({
success: false,
error: 'Route not found',
message: `Route ${req.method} ${req.originalUrl} not found in products router`,
availableRoutes: [
'GET /api/products',
'GET /api/products/api-test',
'POST /api/products/import/excel/preview',
'POST /api/products/import/excel'
],
debug: {
method: req.method,
url: req.url,
originalUrl: req.originalUrl,
baseUrl: req.baseUrl
}
});
});
module.exports = router;