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;