776 lines
20 KiB
JavaScript
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; |