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