2 * @file routes/products.js
3 * @module routes/products
4 * @description Product catalog management API for MSP service offerings and inventory. Handles
5 * product CRUD operations, stock tracking, reorder alerts, and pricing management. Products can
6 * be physical inventory items (stock-managed) or services/subscriptions (non-stock).
9 * - **Stock-Managed (is_stock=true):** Physical inventory with quantity tracking, min stock alerts
10 * - **Non-Stock (is_stock=false):** Services, subscriptions, one-time fees (no inventory tracking)
12 * **Stock Management:**
13 * - Automatic quantity adjustments via invoice operations
14 * - Min stock threshold triggers reorder notifications
15 * - Real-time stock levels visible in product details
16 * - Reorder list endpoint for procurement workflow
19 * - Hardware inventory (laptops, network equipment, peripherals)
20 * - Software licenses (quantity-based tracking)
21 * - Service catalog items (monthly managed services, hourly rates)
22 * - Recurring subscriptions (per-user charges, flat fees)
23 * - One-time fees (setup, installation, consulting)
25 * **Multi-Tenant Architecture:**
26 * - Products scoped to specific tenant (MSP customer organization)
27 * - Root MSP tenant can manage products for all tenants
28 * - Tenant-level product catalogs for white-label offerings
29 * - Shared catalog items across tenant groups
32 * - price_retail: Standard retail price per unit
33 * - Used in invoice line items (can be overridden per invoice)
34 * - Future: cost_price, markup percentages, tiered pricing
37 * - All routes require authentication (authenticateToken)
38 * - Multi-tenant filtering on all queries
39 * - Root admin only for delete operations
40 * - Referential integrity checks prevent orphaned records
43 * - products table (product_id, name, is_stock, stock_quantity, min_stock, price_retail, tenant_id)
44 * - invoice_items table (stock deductions on invoice creation)
45 * - contract_line_items table (recurring product billing)
46 * - purchase_order_items table (vendor purchasing for restock)
47 * - notifications table (low stock alerts)
49 * @requires ../services/db
50 * @requires ../middleware/auth
51 * @requires ../middleware/tenant
52 * @author IBG MSP Development Team
55 * @see {@link module:routes/invoices} for invoice line items
56 * @see {@link module:routes/contracts} for recurring product billing
59const express = require('express');
60const router = express.Router();
61const pool = require('../services/db');
62const authenticateToken = require('../middleware/auth');
63const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
65// Apply authentication and tenant context to all routes
66router.use(authenticateToken, setTenantContext);
69 * @api {get} /products/reorder Get Reorder List
70 * @apiName GetReorderProducts
72 * @apiDescription Retrieves all stock-managed products (is_stock=true) that are below their
73 * minimum stock threshold (min_stock). Sorted by shortage quantity (most urgent first) and
74 * then by product name. Used for procurement workflow and inventory management.
77 * - Only includes products with is_stock=true
78 * - Filters where stock_quantity < min_stock
79 * - Calculates needed_qty = max(min_stock - stock_quantity, 0)
80 * - Sorted by shortage severity (highest shortage first)
83 * - Daily procurement review
84 * - Purchase order generation
85 * - Inventory restocking workflow
86 * - Low stock dashboard alerts
88 * **Tenant Filtering:**
89 * Root MSP users see all tenants' products needing reorder. Regular users see only their
90 * tenant's products. Each product includes tenant_name for multi-tenant views.
91 * @apiHeader {string} Authorization Bearer JWT token
92 * @apiSuccess {object[]} products Array of products needing reorder
93 * @apiSuccess {number} products.product_id Unique product ID
94 * @apiSuccess {string} products.name Product name
95 * @apiSuccess {string} products.description Product description
96 * @apiSuccess {string} products.supplier Supplier/vendor name
97 * @apiSuccess {string} products.sku Product SKU/part number
98 * @apiSuccess {boolean} products.is_stock Always true (stock-managed products only)
99 * @apiSuccess {number} products.stock_quantity Current stock level
100 * @apiSuccess {number} products.min_stock Minimum stock threshold
101 * @apiSuccess {number} products.price_retail Retail price per unit
102 * @apiSuccess {number} products.tenant_id Associated tenant ID
103 * @apiSuccess {string} products.tenant_name Tenant name (joined)
104 * @apiSuccess {number} products.needed_qty Calculated quantity needed (min_stock - stock_quantity)
105 * @apiError {number} 500 Server error during reorder list retrieval
106 * @apiExample {curl} Example Request:
108 * -H "Authorization: Bearer eyJhbGc..." \
109 * "https://api.everydaytech.au/products/reorder"
110 * @apiExample {json} Success Response:
116 * "name": "Dell Latitude 5420 Laptop",
117 * "description": "14" Intel i5, 16GB RAM, 512GB SSD",
118 * "supplier": "Dell Direct",
119 * "sku": "LAT-5420-I5-16-512",
121 * "stock_quantity": 2,
123 * "price_retail": 1299.00,
125 * "tenant_name": "MSP Alpha",
130 * "name": "HP EliteDesk 800 G6 Desktop",
131 * "description": "Intel i7, 32GB RAM, 1TB NVMe",
132 * "supplier": "HP Enterprise",
133 * "sku": "ED-800-G6-I7-32-1T",
135 * "stock_quantity": 3,
137 * "price_retail": 1599.00,
139 * "tenant_name": "MSP Alpha",
146 * @see {@link module:routes/products.getProducts} for full product catalog
147 * @see {@link module:routes/invoices.createInvoice} for stock deductions
149router.get('/reorder', async (req, res) => {
151 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'p', true);
152 const whereClause = tenantClause ? `WHERE ${tenantClause} AND` : 'WHERE';
154 const result = await pool.query(
157 t.name AS tenant_name,
158 GREATEST(p.min_stock - p.stock_quantity, 0) AS needed_qty
160 LEFT JOIN tenants t ON p.tenant_id = t.tenant_id
161 ${whereClause} p.is_stock = true AND p.stock_quantity < p.min_stock
162 ORDER BY (p.min_stock - p.stock_quantity) DESC, p.name ASC`,
165 res.json({ products: result.rows });
167 console.error('Error fetching products for reorder', err);
168 res.status(500).send('Server error');
173 * @api {get} /products List Products
174 * @apiName GetProducts
176 * @apiDescription Retrieves paginated product catalog with search capabilities. Searches across
177 * product name, description, supplier, and SKU fields. Returns both stock-managed inventory
178 * items and non-stock service catalog items.
181 * Fuzzy ILIKE search across:
184 * - Supplier/vendor name
188 * - Default: 25 items per page
189 * - Returns total count for pagination UI
190 * - Sorted alphabetically by product name
192 * **Tenant Filtering:**
193 * Products filtered by authenticated user's tenant. Root MSP users see all tenants' products
194 * with tenant_name included. Regular users see only their tenant's products.
196 * **Response Format:**
197 * Each product includes complete details (stock levels, pricing, supplier info) plus joined
198 * tenant_name for multi-tenant views.
199 * @apiHeader {string} Authorization Bearer JWT token
200 * @apiParam {number} [page=1] Page number for pagination
201 * @apiParam {number} [limit=25] Items per page (default: 25)
202 * @apiParam {string} [search] Search term (name, description, supplier, SKU)
203 * @apiSuccess {object[]} products Array of product objects
204 * @apiSuccess {number} products.product_id Unique product ID
205 * @apiSuccess {string} products.name Product name
206 * @apiSuccess {string} products.description Product description
207 * @apiSuccess {string} products.supplier Supplier/vendor name (may be NULL)
208 * @apiSuccess {string} products.sku Product SKU/part number (may be NULL)
209 * @apiSuccess {boolean} products.is_stock Whether product is stock-managed (true) or service (false)
210 * @apiSuccess {number} products.stock_quantity Current stock level (0 for non-stock products)
211 * @apiSuccess {number} products.min_stock Minimum stock threshold (0 for non-stock products)
212 * @apiSuccess {number} products.price_retail Retail price per unit
213 * @apiSuccess {number} products.tenant_id Associated tenant ID
214 * @apiSuccess {string} products.tenant_name Tenant name (joined)
215 * @apiSuccess {string} products.created_at Timestamp when product was created
216 * @apiSuccess {Number} total Total count of products matching filters (for pagination)
218 * @apiError {Number} 500 Server error during product retrieval
220 * @apiExample {curl} Example Request:
222 * -H "Authorization: Bearer eyJhbGc..." \
223 * "https://api.everydaytech.au/products?search=laptop&page=1&limit=25"
225 * @apiExample {json} Success Response:
231 * "name": "Dell Latitude 5420 Laptop",
232 * "description": "14" Intel i5, 16GB RAM, 512GB SSD",
233 * "supplier": "Dell Direct",
234 * "sku": "LAT-5420-I5-16-512",
236 * "stock_quantity": 12,
238 * "price_retail": 1299.00,
240 * "tenant_name": "MSP Alpha",
241 * "created_at": "2024-01-15T10:00:00.000Z"
245 * "name": "Managed Services - Per User",
246 * "description": "Monthly managed IT services per user",
248 * "sku": "SVC-MANAGED-USER",
250 * "stock_quantity": 0,
252 * "price_retail": 125.00,
254 * "tenant_name": "MSP Alpha",
255 * "created_at": "2024-01-10T09:00:00.000Z"
262 * @see {@link module:routes/products.getReorderProducts} for reorder list
263 * @see {@link module:routes/products.getProduct} for single product details
265router.get('/', async (req, res) => {
266 // Debug: log request context
267 console.log(`[API][products] GET /products | Tenant:`, req.tenant, '| User:', req.user ? req.user.id : null, '| Query:', req.query);
269 const { page = 1, limit = 25, search } = req.query;
270 const offset = (parseInt(page, 10) - 1) * parseInt(limit, 10);
272 const { clause: tenantClause, params: tenantParams, nextParamIndex } = getTenantFilter(req, 'p', true);
273 let where = tenantClause ? `WHERE ${tenantClause}` : '';
274 const params = [...tenantParams];
275 let paramIndex = nextParamIndex;
277 if (search && search.trim() !== '') {
278 params.push(`%${search.trim()}%`);
279 const searchWhere = `(
280 p.name ILIKE $${paramIndex} OR
281 p.description ILIKE $${paramIndex} OR
282 p.supplier ILIKE $${paramIndex} OR
283 p.sku ILIKE $${paramIndex})`;
284 where += where ? ` AND ${searchWhere}` : `WHERE ${searchWhere}`;
288 const totalQ = `SELECT COUNT(*) FROM products p ${where}`;
289 // Debug: log final query and params
290 console.log(`[API][products] Executing totalQ:`, totalQ, '| Params:', params);
292 const totalRes = await pool.query(totalQ, params);
293 const total = parseInt(totalRes.rows[0].count, 10);
297 const q = `SELECT p.*, t.name AS tenant_name FROM products p LEFT JOIN tenants t ON p.tenant_id = t.tenant_id ${where} ORDER BY p.name ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
298 console.log(`[API][products] Executing q:`, q, '| Params:', params);
299 const result = await pool.query(q, params);
300 res.json({ products: result.rows, total });
302 console.error('[API][products] Error fetching products:', err, '| Tenant:', req.tenant, '| Query:', req.query);
303 res.status(500).json({ error: 'Server error', details: err.message });
308 * @api {get} /products/:id Get Product
309 * @apiName GetProduct
311 * @apiDescription Retrieves a single product by ID with all details. Enforces tenant isolation.
313 * **Tenant Validation:**
314 * Product must belong to user's tenant scope. Root MSP users can access all products.
315 * Regular users limited to their tenant's products.
316 * @apiHeader {string} Authorization Bearer JWT token
317 * @apiParam {number} id Product ID (in URL path)
318 * @apiSuccess {number} product_id Unique product ID
319 * @apiSuccess {string} name Product name
320 * @apiSuccess {string} description Product description
321 * @apiSuccess {string} supplier Supplier/vendor name (may be NULL)
322 * @apiSuccess {string} sku Product SKU/part number (may be NULL)
323 * @apiSuccess {boolean} is_stock Whether product is stock-managed
324 * @apiSuccess {number} stock_quantity Current stock level
325 * @apiSuccess {number} min_stock Minimum stock threshold
326 * @apiSuccess {number} price_retail Retail price per unit
327 * @apiSuccess {number} tenant_id Associated tenant ID
328 * @apiSuccess {string} created_at Creation timestamp
329 * @apiError {number} 404 Product not found (or not in user's tenant)
330 * @apiError {number} 500 Server error during retrieval
331 * @apiExample {curl} Example Request:
333 * -H "Authorization: Bearer eyJhbGc..." \
334 * "https://api.everydaytech.au/products/42"
336 * @apiExample {json} Success Response:
340 * "name": "Dell Latitude 5420 Laptop",
341 * "description": "14" Intel i5, 16GB RAM, 512GB SSD",
342 * "supplier": "Dell Direct",
343 * "sku": "LAT-5420-I5-16-512",
345 * "stock_quantity": 12,
347 * "price_retail": 1299.00,
349 * "created_at": "2024-01-15T10:00:00.000Z"
353 * @see {@link module:routes/products.updateProduct} for updating products
355router.get('/:id', async (req, res) => {
357 const { id } = req.params;
358 // Enforce tenant filter for single product fetch
359 const { clause: tenantClause, params: tenantParams, nextParamIndex } = getTenantFilter(req, 'p', true);
360 let query = 'SELECT * FROM products p WHERE product_id = $1';
363 // Shift tenant param placeholders to start at $2 because $1 is the product_id
364 const adjustedClause = tenantClause.replace(/\$(\d+)/g, (m, n) => `$${parseInt(n, 10) + 1}`);
365 query += ` AND ${adjustedClause}`;
366 params.push(...tenantParams);
368 const result = await pool.query(query, params);
369 if (result.rows.length === 0) return res.status(404).send('Not found');
370 res.json(result.rows[0]);
372 console.error('Error fetching product', err);
373 res.status(500).send('Server error');
378 * @api {post} /products Create Product
379 * @apiName CreateProduct
381 * @apiDescription Creates a new product in the catalog. Automatically associated with authenticated
382 * user's tenant. Can create stock-managed inventory items or non-stock service catalog items.
384 * **Stock-Managed Products:**
385 * Set is_stock=true and provide stock_quantity and min_stock values. Stock will be tracked
386 * automatically via invoice operations.
388 * **Service Catalog Items:**
389 * Set is_stock=false. Stock fields ignored (set to 0). Used for services, subscriptions,
390 * labor hours, one-time fees, etc.
392 * **Tenant Association:**
393 * Product automatically associated with req.tenant.id from authenticated user's context.
394 * Tenant context required (returns 403 if missing).
395 * @apiHeader {string} Authorization Bearer JWT token
396 * @apiHeader {string} Content-Type application/json
397 * @apiParam {string} name Product name (required)
398 * @apiParam {string} [description] Product description (default: empty string)
399 * @apiParam {string} [supplier] Supplier/vendor name (default: NULL)
400 * @apiParam {boolean} [is_stock=false] Whether product is stock-managed (default: false)
401 * @apiParam {number} [stock_quantity=0] Initial stock quantity (default: 0)
402 * @apiParam {number} [min_stock=0] Minimum stock threshold for reorder alerts (default: 0)
403 * @apiParam {number} [price_retail=0] Retail price per unit (default: 0)
404 * @apiSuccess {number} product_id Generated product ID
405 * @apiSuccess {string} name Product name
406 * @apiSuccess {string} description Product description
407 * @apiSuccess {boolean} is_stock Stock-managed flag
408 * @apiSuccess {number} stock_quantity Current stock level
409 * @apiSuccess {number} tenant_id Associated tenant ID
410 * @apiSuccess {string} created_at Creation timestamp
411 * @apiError {Number} 403 No tenant context (authentication issue)
412 * @apiError {Number} 500 Create failed
414 * @apiExample {curl} Example Request (Stock Item):
416 * -H "Authorization: Bearer eyJhbGc..." \
417 * -H "Content-Type: application/json" \
419 * "name": "Dell Latitude 5420 Laptop",
420 * "description": "14" Intel i5, 16GB RAM, 512GB SSD",
421 * "supplier": "Dell Direct",
422 * "sku": "LAT-5420-I5-16-512",
424 * "stock_quantity": 15,
426 * "price_retail": 1299.00
428 * "https://api.everydaytech.au/products"
430 * @apiExample {curl} Example Request (Service Item):
432 * -H "Authorization: Bearer eyJhbGc..." \
433 * -H "Content-Type: application/json" \
435 * "name": "Managed Services - Per User",
436 * "description": "Monthly managed IT services per user",
437 * "sku": "SVC-MANAGED-USER",
439 * "price_retail": 125.00
441 * "https://api.everydaytech.au/products"
443 * @apiExample {json} Success Response:
444 * HTTP/1.1 201 Created
447 * "name": "Dell Latitude 5420 Laptop",
448 * "description": "14" Intel i5, 16GB RAM, 512GB SSD",
449 * "supplier": "Dell Direct",
450 * "sku": "LAT-5420-I5-16-512",
452 * "stock_quantity": 15,
454 * "price_retail": 1299.00,
456 * "created_at": "2024-03-11T16:00:00.000Z"
460 * @see {@link module:routes/products.updateProduct} for updating products
461 * @see {@link module:routes/invoices.createInvoice} for using products in invoices
463router.post('/', async (req, res) => {
466 const tenantId = req.tenant?.id;
467 if (!tenantId) return res.status(403).json({ error: 'No tenant context' });
468 const result = await pool.query(
469 `INSERT INTO products (name, description, supplier, is_stock, stock_quantity, min_stock, price_retail, tenant_id)
470 VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
471 [p.name, p.description || '', p.supplier || null, p.is_stock || false, p.stock_quantity || 0, p.min_stock || 0, p.price_retail || 0, tenantId]
473 res.status(201).json(result.rows[0]);
475 console.error('Error creating product', err);
476 res.status(500).send('Create failed');
481 * @api {put} /products/:id Update Product
482 * @apiName UpdateProduct
484 * @apiDescription Updates an existing product. Validates tenant ownership before updating.
485 * All fields provided in request body will be updated.
487 * **Tenant Validation:**
488 * Product must belong to user's tenant. Returns 404 if product not found or not in tenant scope.
490 * **Stock Adjustments:**
491 * Can manually adjust stock_quantity to correct inventory levels. Use with caution as this
492 * bypasses normal invoice-based stock tracking.
495 * All fields from request body applied. Provide complete values for fields being updated.
496 * Missing optional fields default to empty/zero values.
497 * @apiHeader {string} Authorization Bearer JWT token
498 * @apiHeader {string} Content-Type application/json
499 * @apiParam {number} id Product ID (in URL path)
500 * @apiParam {string} name Product name (required)
501 * @apiParam {string} [description] Product description (defaults to empty string if omitted)
502 * @apiParam {string} [supplier] Supplier/vendor name (defaults to NULL if omitted)
503 * @apiParam {boolean} [is_stock=false] Whether product is stock-managed
504 * @apiParam {number} [stock_quantity=0] Current stock quantity
505 * @apiParam {number} [min_stock=0] Minimum stock threshold
506 * @apiParam {number} [price_retail=0] Retail price per unit
507 * @apiSuccess {number} product_id Product ID
508 * @apiSuccess {string} name Updated product name
509 * @apiSuccess {string} description Updated description
510 * @apiSuccess {boolean} is_stock Stock-managed flag
511 * @apiSuccess {number} stock_quantity Updated stock level
512 * @apiSuccess {number} price_retail Updated retail price
513 * @apiError {Number} 404 Product not found (or not in user's tenant)
514 * @apiError {Number} 500 Update failed
516 * @apiExample {curl} Example Request:
518 * -H "Authorization: Bearer eyJhbGc..." \
519 * -H "Content-Type: application/json" \
521 * "name": "Dell Latitude 5420 Laptop",
522 * "description": "14" Intel i5, 16GB RAM, 512GB SSD - Updated specs",
523 * "supplier": "Dell Direct",
525 * "stock_quantity": 18,
527 * "price_retail": 1349.00
529 * "https://api.everydaytech.au/products/42"
531 * @apiExample {json} Success Response:
535 * "name": "Dell Latitude 5420 Laptop",
536 * "description": "14" Intel i5, 16GB RAM, 512GB SSD - Updated specs",
537 * "supplier": "Dell Direct",
539 * "stock_quantity": 18,
541 * "price_retail": 1349.00,
546 * @see {@link module:routes/products.createProduct} for creating products
548router.put('/:id', async (req, res) => {
550 const { id } = req.params;
553 // Check tenant access
554 const { clause: tenantClause, params: tenantParams, nextParamIndex } = getTenantFilter(req, 'p', true);
555 let checkQuery = `SELECT product_id FROM products p WHERE product_id = $${nextParamIndex}`;
556 const checkParams = [...tenantParams, id];
558 checkQuery = `SELECT product_id FROM products p WHERE ${tenantClause} AND product_id = $${nextParamIndex}`;
560 const check = await pool.query(checkQuery, checkParams);
561 if (check.rows.length === 0) return res.status(404).send('Not found');
563 const result = await pool.query(
564 `UPDATE products SET name=$1, description=$2, supplier=$3, is_stock=$4, stock_quantity=$5, min_stock=$6, price_retail=$7 WHERE product_id=$8 RETURNING *`,
565 [p.name, p.description || '', p.supplier || null, p.is_stock || false, p.stock_quantity || 0, p.min_stock || 0, p.price_retail || 0, id]
567 res.json(result.rows[0]);
569 console.error('Error updating product:', err);
570 console.error('Error details:', err.message);
571 res.status(500).json({ error: 'Update failed', details: err.message });
576 * @api {delete} /products/:id Delete Product
577 * @apiName DeleteProduct
579 * @apiDescription Permanently deletes a product from the catalog. Root tenant admin only.
580 * Validates referential integrity - cannot delete products referenced in invoices, contracts,
581 * or purchase orders.
584 * Only root tenant administrators (MSP level) can delete products. Regular tenant users
585 * receive 403 Forbidden.
587 * **Authorization Checks:**
588 * - req.tenant.subdomain === 'admin' OR
589 * - req.tenant.isMsp === true OR
590 * - req.user.is_msp === true
592 * **Referential Integrity:**
593 * System checks for product references in:
594 * - invoice_line_items table
595 * - contract_line_items table
596 * - purchase_order_items table
598 * If ANY references exist, deletion blocked with detailed error message showing counts.
600 * **Constraint Handling:**
601 * Additional database-level foreign key constraint protection. If constraint violation occurs,
602 * user-friendly error returned instead of raw database error.
605 * Successful deletions logged to console with product ID, name, and user email.
606 * @apiHeader {string} Authorization Bearer JWT token (root admin required)
607 * @apiParam {number} id Product ID (in URL path)
608 * @apiSuccess {number} 204 No Content - Product deleted successfully (empty response body)
609 * @apiError {number} 400 Cannot delete product (has references in invoices/contracts/POs)
610 * @apiError {number} 403 Access denied (not root tenant administrator)
611 * @apiError {number} 404 Product not found
612 * @apiError {number} 500 Delete failed (database error)
613 * @apiExample {curl} Example Request:
615 * -H "Authorization: Bearer eyJhbGc..." \
616 * "https://api.everydaytech.au/products/42"
617 * @apiExample {json} Success Response:
618 * HTTP/1.1 204 No Content
619 * @apiExample {json} Error Response (Has References):
620 * HTTP/1.1 400 Bad Request
622 * "error": "Cannot delete product",
623 * "message": "This product is referenced in 3 invoice(s), 2 contract(s). Please remove these references first or contact support."
625 * @apiExample {json} Error Response (Not Admin):
626 * HTTP/1.1 403 Forbidden
628 * "error": "Access denied",
629 * "message": "Only root tenant administrators can delete products"
632 * @see {@link module:routes/products.updateProduct} for updating products
634router.delete('/:id', async (req, res) => {
636 const { id } = req.params;
638 // Check if user is root tenant admin
639 const isRootAdmin = req.tenant?.subdomain === 'admin' || req.tenant?.isMsp === true || req.user?.is_msp === true;
642 return res.status(403).json({
643 error: 'Access denied',
644 message: 'Only root tenant administrators can delete products'
648 // Check if product exists
649 const checkResult = await pool.query(
650 'SELECT product_id, name FROM products WHERE product_id = $1',
654 if (checkResult.rows.length === 0) {
655 return res.status(404).json({ error: 'Product not found' });
658 const productName = checkResult.rows[0].name;
660 // Check for references in other tables
661 const references = await pool.query(`
663 (SELECT COUNT(*) FROM invoice_line_items WHERE product_id = $1) as invoice_items,
664 (SELECT COUNT(*) FROM contract_line_items WHERE product_id = $1) as contract_items,
665 (SELECT COUNT(*) FROM purchase_order_items WHERE product_id = $1) as po_items
668 const refs = references.rows[0];
669 const totalRefs = parseInt(refs.invoice_items) + parseInt(refs.contract_items) + parseInt(refs.po_items);
673 if (parseInt(refs.invoice_items) > 0) details.push(`${refs.invoice_items} invoice(s)`);
674 if (parseInt(refs.contract_items) > 0) details.push(`${refs.contract_items} contract(s)`);
675 if (parseInt(refs.po_items) > 0) details.push(`${refs.po_items} purchase order(s)`);
677 return res.status(400).json({
678 error: 'Cannot delete product',
679 message: `This product is referenced in ${details.join(', ')}. Please remove these references first or contact support.`
683 // Delete the product
684 await pool.query('DELETE FROM products WHERE product_id = $1', [id]);
686 console.log(`[Products] Deleted product ${id} (${productName}) by user ${req.user.email}`);
689 console.error('Error deleting product', err);
691 // Handle foreign key constraint errors
692 if (err.code === '23503') {
693 return res.status(400).json({
694 error: 'Cannot delete product',
695 message: 'This product is still referenced in other records. Please remove all references first.'
699 res.status(500).json({ error: 'Delete failed', details: err.message });
703module.exports = router;