first commit
This commit is contained in:
264
src/services/inventoryService.js
Normal file
264
src/services/inventoryService.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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,
|
||||
};
|
||||
Reference in New Issue
Block a user