// services/inventoryService.js - COMPLETE INVENTORY SYSTEM const { prisma } = require('../config/database'); const Product = require('../models/mongodb/Product'); /** * ✅ Auto-reduce stock when order is DELIVERED */ async function reduceStockOnDelivery(orderId) { try { const order = await prisma.order.findUnique({ where: { id: orderId }, include: { items: true }, }); if (!order || order.status !== 'DELIVERED') return null; const results = []; for (const item of order.items) { const product = await Product.findById(item.productId); if (!product) continue; let previousStock, newStock; if (product.hasVariants && product.variants?.length > 0) { // ✅ Reduce from matching variant by SKU previousStock = product.variants .filter(v => v.isActive) .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); const variantIndex = product.variants.findIndex( v => v.sku === item.productSku ); if (variantIndex !== -1) { const currentQty = product.variants[variantIndex].inventory?.quantity || 0; product.variants[variantIndex].inventory.quantity = Math.max( 0, currentQty - item.quantity ); await product.save(); } newStock = product.variants .filter(v => v.isActive) .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); } else { // ✅ Non-variant product previousStock = product.stock || 0; newStock = Math.max(0, previousStock - item.quantity); await Product.findByIdAndUpdate(item.productId, { stock: newStock, updatedAt: new Date(), }); } await prisma.inventoryLog.create({ data: { productId: item.productId, productName: item.productName || product.name, type: 'SOLD', quantityChange: -item.quantity, previousStock, newStock, orderId, notes: `Order ${order.orderNumber} delivered`, }, }); results.push({ productId: item.productId, productName: product.name, reduced: item.quantity, previousStock, newStock, }); } return results; } catch (error) { console.error('❌ Stock reduction error:', error); throw error; } } /** * ✅ Get low stock products */ async function getLowStockProducts(threshold = 10) { try { const products = await Product.find({ status: 'active' }).lean(); const withStock = products.map(product => { let totalStock = 0; if (product.hasVariants && product.variants?.length > 0) { totalStock = product.variants .filter(v => v.isActive) .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); } else { totalStock = product.stock || 0; } return { _id: product._id.toString(), name: product.name, slug: product.slug, stock: totalStock, basePrice: product.basePrice, hasVariants: product.hasVariants, status: totalStock === 0 ? 'OUT_OF_STOCK' : totalStock <= 5 ? 'CRITICAL' : 'LOW', displayImage: getProductImage(product), }; }); return withStock .filter(p => p.stock <= threshold) .sort((a, b) => a.stock - b.stock) .slice(0, threshold); } catch (error) { console.error('Error fetching low stock:', error); return []; } } /** * ✅ Get inventory stats for dashboard */ async function getInventoryStats() { try { const products = await Product.find({ status: 'active' }).lean(); let outOfStock = 0, criticalStock = 0, lowStock = 0, inStock = 0; products.forEach(product => { let stock = 0; if (product.hasVariants && product.variants?.length > 0) { stock = product.variants .filter(v => v.isActive) .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); } else { stock = product.stock || 0; } if (stock === 0) outOfStock++; else if (stock <= 5) criticalStock++; else if (stock <= 10) lowStock++; else inStock++; }); return { totalProducts: products.length, outOfStock, criticalStock, lowStock, inStock, }; } catch (error) { console.error('Error fetching inventory stats:', error); return null; } } /** * ✅ Manual stock adjustment (Admin) */ async function adjustStock( productId, variantSku = null, quantity, type, notes, adminId ) { try { const product = await Product.findById(productId); if (!product) throw new Error('Product not found'); let previousStock, newStock; if (product.hasVariants && product.variants?.length > 0 && variantSku) { // ✅ Adjust specific variant const variantIndex = product.variants.findIndex( v => v.sku === variantSku ); if (variantIndex === -1) throw new Error('Variant not found'); const currentQty = product.variants[variantIndex].inventory?.quantity || 0; previousStock = currentQty; if (type === 'ADD') newStock = currentQty + quantity; else if (type === 'REMOVE') newStock = Math.max(0, currentQty - quantity); else if (type === 'SET') newStock = quantity; else throw new Error('Invalid type'); product.variants[variantIndex].inventory.quantity = newStock; await product.save(); } else { // ✅ Non-variant product previousStock = product.stock || 0; if (type === 'ADD') newStock = previousStock + quantity; else if (type === 'REMOVE') newStock = Math.max(0, previousStock - quantity); else if (type === 'SET') newStock = quantity; else throw new Error('Invalid type'); await Product.findByIdAndUpdate(productId, { stock: newStock, updatedAt: new Date(), }); } await prisma.inventoryLog.create({ data: { productId, productName: product.name, type: type === 'ADD' ? 'RESTOCK' : 'ADJUSTMENT', quantityChange: type === 'ADD' ? quantity : -quantity, previousStock, newStock, notes: notes || `Manual ${type} by admin`, adjustedBy: adminId, }, }); return { success: true, productName: product.name, previousStock, newStock, }; } catch (error) { console.error('Error adjusting stock:', error); throw error; } } function getProductImage(product) { return ( product.images?.gallery?.[0] || product.images?.primary || product.variants?.[0]?.images?.[0] || 'https://via.placeholder.com/300' ); } module.exports = { reduceStockOnDelivery, getLowStockProducts, getInventoryStats, adjustStock, };