Files
eCommerce-backend/src/controllers/products/recommendation.js
2026-02-19 17:25:38 +05:30

345 lines
8.9 KiB
JavaScript

// controllers/products/recommendationController.js
const Product = require('../../models/mongodb/Product');
const { prisma } = require('../../config/database');
/**
* @desc Get recommendations for a product
* @route GET /api/products/:slug/recommendations
* @access Public
*/
const getProductRecommendations = async (req, res, next) => {
try {
const { slug } = req.params;
const { limit = 12 } = req.query;
// Get the current product
const currentProduct = await Product.findOne({ slug, status: 'active' });
if (!currentProduct) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}
// Build recommendation query
const recommendations = await getRecommendedProducts(
currentProduct,
parseInt(limit)
);
res.status(200).json({
success: true,
count: recommendations.length,
data: recommendations,
});
} catch (error) {
console.error('Get recommendations error:', error);
next(error);
}
};
/**
* @desc Get "Customers also bought" products
* @route GET /api/products/:slug/also-bought
* @access Public
*/
const getAlsoBoughtProducts = async (req, res, next) => {
try {
const { slug } = req.params;
const { limit = 8 } = req.query;
const currentProduct = await Product.findOne({ slug, status: 'active' });
if (!currentProduct) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}
// Get products frequently bought together
const alsoBought = await getFrequentlyBoughtTogether(
currentProduct._id.toString(),
parseInt(limit)
);
res.status(200).json({
success: true,
count: alsoBought.length,
data: alsoBought,
});
} catch (error) {
console.error('Get also bought error:', error);
next(error);
}
};
/**
* @desc Get similar products
* @route GET /api/products/:slug/similar
* @access Public
*/
const getSimilarProducts = async (req, res, next) => {
try {
const { slug } = req.params;
const { limit = 10 } = req.query;
const currentProduct = await Product.findOne({ slug, status: 'active' });
if (!currentProduct) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}
// Get similar products based on attributes
const similar = await getSimilarProductsByAttributes(
currentProduct,
parseInt(limit)
);
res.status(200).json({
success: true,
count: similar.length,
data: similar,
});
} catch (error) {
console.error('Get similar products error:', error);
next(error);
}
};
/**
* @desc Get personalized recommendations for user
* @route GET /api/products/recommendations/personalized
* @access Private
*/
const getPersonalizedRecommendations = async (req, res, next) => {
try {
const userId = req.user?.id;
const { limit = 20 } = req.query;
if (!userId) {
// Return popular products for non-authenticated users
const popularProducts = await Product.find({ status: 'active' })
.sort({ purchaseCount: -1, viewCount: -1 })
.limit(parseInt(limit));
return res.status(200).json({
success: true,
count: popularProducts.length,
data: popularProducts,
});
}
// Get user's purchase history
const userOrders = await prisma.order.findMany({
where: {
userId,
status: 'DELIVERED',
},
include: {
items: true,
},
take: 10,
orderBy: { createdAt: 'desc' },
});
// Get user's wishlist
const wishlist = await prisma.wishlistItem.findMany({
where: { userId },
take: 20,
});
// Extract product IDs
const purchasedProductIds = userOrders.flatMap((order) =>
order.items.map((item) => item.productId)
);
const wishlistProductIds = wishlist.map((item) => item.productId);
// Get categories and tags from purchased products
const purchasedProducts = await Product.find({
_id: { $in: purchasedProductIds },
});
const categories = [...new Set(purchasedProducts.map((p) => p.category))];
const tags = [
...new Set(purchasedProducts.flatMap((p) => p.tags || [])),
];
// Build personalized recommendations
const recommendations = await Product.find({
status: 'active',
_id: {
$nin: [...purchasedProductIds, ...wishlistProductIds],
},
$or: [
{ category: { $in: categories } },
{ tags: { $in: tags } },
{ isFeatured: true },
],
})
.sort({ purchaseCount: -1, viewCount: -1 })
.limit(parseInt(limit));
res.status(200).json({
success: true,
count: recommendations.length,
data: recommendations,
});
} catch (error) {
console.error('Get personalized recommendations error:', error);
next(error);
}
};
/**
* Helper Functions
*/
// Get recommended products based on multiple factors
async function getRecommendedProducts(currentProduct, limit) {
const priceRange = {
min: currentProduct.basePrice * 0.7,
max: currentProduct.basePrice * 1.3,
};
// Score-based recommendation
const recommendations = await Product.aggregate([
{
$match: {
_id: { $ne: currentProduct._id },
status: 'active',
},
},
{
$addFields: {
score: {
$add: [
{ $cond: [{ $eq: ['$category', currentProduct.category] }, 50, 0] },
{
$cond: [
{
$and: [
{ $gte: ['$basePrice', priceRange.min] },
{ $lte: ['$basePrice', priceRange.max] },
],
},
30,
0,
],
},
{
$multiply: [
{
$size: {
$ifNull: [
{
$setIntersection: [
{ $ifNull: ['$tags', []] },
currentProduct.tags || [],
],
},
[],
],
},
},
5,
],
},
{ $cond: ['$isFeatured', 20, 0] },
{ $divide: [{ $ifNull: ['$viewCount', 0] }, 100] },
{ $divide: [{ $multiply: [{ $ifNull: ['$purchaseCount', 0] }, 10] }, 10] },
],
},
},
},
{ $sort: { score: -1 } },
{ $limit: limit },
]);
return recommendations;
}
// Get frequently bought together products
async function getFrequentlyBoughtTogether(productId, limit) {
try {
const ordersWithProduct = await prisma.orderItem.findMany({
where: { productId },
select: { orderId: true },
distinct: ['orderId'],
});
if (ordersWithProduct.length === 0) {
const product = await Product.findById(productId);
return await Product.find({
_id: { $ne: productId },
category: product.category,
status: 'active',
})
.sort({ purchaseCount: -1 })
.limit(limit);
}
const orderIds = ordersWithProduct.map((item) => item.orderId);
const otherProducts = await prisma.orderItem.findMany({
where: {
orderId: { in: orderIds },
productId: { not: productId },
},
select: { productId: true },
});
const productFrequency = {};
otherProducts.forEach((item) => {
productFrequency[item.productId] =
(productFrequency[item.productId] || 0) + 1;
});
const sortedProductIds = Object.entries(productFrequency)
.sort(([, a], [, b]) => b - a)
.slice(0, limit)
.map(([id]) => id);
const products = await Product.find({
_id: { $in: sortedProductIds },
status: 'active',
});
return products;
} catch (error) {
console.error('Get frequently bought together error:', error);
return [];
}
}
// Get similar products by attributes
async function getSimilarProductsByAttributes(currentProduct, limit) {
const similar = await Product.find({
_id: { $ne: currentProduct._id },
status: 'active',
$or: [
{ category: currentProduct.category },
{ tags: { $in: currentProduct.tags || [] } },
{ brand: currentProduct.brand },
],
})
.sort({
purchaseCount: -1,
viewCount: -1,
})
.limit(limit);
return similar;
}
module.exports = {
getProductRecommendations,
getAlsoBoughtProducts,
getSimilarProducts,
getPersonalizedRecommendations,
};