Files
vaishnavi-ecommerce-backend/src/services/inventoryService.js
2026-03-10 12:43:27 +05:30

265 lines
6.8 KiB
JavaScript

// 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,
};