const { prisma } = require('../config/database'); const Product = require('../models/mongodb/Product'); const { RETURN_WINDOW_DAYS, ALLOWED_STATUSES, } = require('../config/returnPolicy'); const { calculateDeliveryDate, } = require('../services/deliveryEstimationService'); const { reduceStockOnDelivery } = require('../services/inventoryService'); // @desc Create new order // @route POST /api/orders // @access Private exports.createOrder = async (req, res, next) => { try { const { items, shippingAddressId, paymentMethod, couponCode } = req.body; const userId = req.user.id; console.log('================================='); console.log('🎟️ COUPON DEBUG'); console.log('Received couponCode:', req.body.couponCode); console.log('Full body:', req.body); console.log('================================='); console.log('📦 Creating order for user:', userId); console.log('🎟️ Coupon code:', couponCode); // Validate items if (!items || items.length === 0) { return res.status(400).json({ success: false, message: 'No items in the order', }); } // Validate shipping address const address = await prisma.address.findUnique({ where: { id: shippingAddressId }, }); const deliveryEstimation = calculateDeliveryDate( address.postalCode, new Date(), 'STANDARD' ); console.log( '📅 Estimated delivery:', deliveryEstimation.estimatedDelivery.formatted ); if (!address || address.userId !== userId) { return res.status(400).json({ success: false, message: 'Invalid shipping address', }); } // Fetch product details from MongoDB const productIds = items.map(item => item.productId); const products = await Product.find({ _id: { $in: productIds } }); if (products.length !== productIds.length) { return res.status(400).json({ success: false, message: 'Some products not found', }); } // Calculate totals let subtotal = 0; const orderItems = []; for (const item of items) { const product = products.find(p => p._id.toString() === item.productId); if (!product) { return res.status(400).json({ success: false, message: `Product ${item.productId} not found`, }); } let price = product.basePrice; let sku = product.slug; if (product.hasVariants && product.variants?.length > 0) { const variant = product.variants.find(v => v.sku === item.sku); if (variant) { price = variant.price; sku = variant.sku; } } const itemTotal = price * item.quantity; subtotal += itemTotal; orderItems.push({ productId: item.productId, productName: product.name, productSku: sku, price: price, quantity: item.quantity, }); } // Calculate tax and shipping const taxRate = 0.18; // 18% GST const taxAmount = subtotal * taxRate; let shippingAmount = subtotal > 500 ? 0 : 50; // Free shipping above ₹500 let discountAmount = 0; let appliedCoupon = null; // ========================================== // VALIDATE AND APPLY COUPON // ========================================== if (couponCode) { console.log('🎟️ Validating coupon:', couponCode); const coupon = await prisma.coupon.findUnique({ where: { code: couponCode.toUpperCase() }, }); if (coupon) { // Validate coupon const now = new Date(); let couponError = null; if (!coupon.isActive) { couponError = 'Coupon is not active'; } else if (now < new Date(coupon.validFrom)) { couponError = 'Coupon is not yet valid'; } else if (now > new Date(coupon.validUntil)) { couponError = 'Coupon has expired'; } else if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) { couponError = 'Coupon usage limit reached'; } else if ( coupon.minOrderAmount && subtotal < parseFloat(coupon.minOrderAmount) ) { couponError = `Minimum order amount of ₹${coupon.minOrderAmount} required`; } if (couponError) { console.log('❌ Coupon validation failed:', couponError); return res.status(400).json({ success: false, message: couponError, }); } // Calculate discount if (coupon.type === 'PERCENTAGE') { discountAmount = (subtotal * parseFloat(coupon.value)) / 100; } else if (coupon.type === 'FIXED_AMOUNT') { discountAmount = Math.min(parseFloat(coupon.value), subtotal); } else if (coupon.type === 'FREE_SHIPPING') { discountAmount = shippingAmount; shippingAmount = 0; } appliedCoupon = coupon; console.log('✅ Coupon applied:', { code: coupon.code, type: coupon.type, discount: discountAmount, }); } else { console.log('❌ Coupon not found'); return res.status(400).json({ success: false, message: 'Invalid coupon code', }); } } // Calculate final total const totalAmount = subtotal + taxAmount + shippingAmount - discountAmount; // Generate unique order number const orderNumber = `ORD${Date.now()}${Math.floor(Math.random() * 1000)}`; // ========================================== // CREATE ORDER IN TRANSACTION // ========================================== const result = await prisma.$transaction(async tx => { // Create order const order = await tx.order.create({ data: { orderNumber, userId, status: 'PENDING', subtotal, taxAmount, shippingAmount, discountAmount, totalAmount, paymentStatus: 'PENDING', paymentMethod: paymentMethod || 'PAYTM', shippingAddressId, items: { create: orderItems, }, }, include: { items: true, address: true, user: { select: { id: true, email: true, firstName: true, lastName: true, phone: true, }, }, }, }); // ✅ INCREMENT COUPON USAGE COUNT if (appliedCoupon) { await tx.coupon.update({ where: { id: appliedCoupon.id }, data: { usedCount: { increment: 1, }, }, }); console.log('✅ Coupon usage incremented:', { code: appliedCoupon.code, previousCount: appliedCoupon.usedCount, newCount: appliedCoupon.usedCount + 1, }); } return order; }); console.log('✅ Order created:', result.id); // Clear user's cart after order creation await prisma.cartItem.deleteMany({ where: { userId }, }); res.status(201).json({ success: true, message: 'Order created successfully', order: result, appliedCoupon: appliedCoupon ? { code: appliedCoupon.code, discount: discountAmount, } : null, deliveryEstimation, }); } catch (error) { console.error('Create order error:', error); next(error); } }; // @desc Get user orders exports.getUserOrders = async (req, res, next) => { try { const { page = 1, limit = 10, status } = req.query; const skip = (page - 1) * limit; const where = { userId: req.user.id }; if (status) where.status = status; const [orders, total] = await Promise.all([ prisma.order.findMany({ where, include: { items: true, address: true }, orderBy: { createdAt: 'desc' }, skip: parseInt(skip), take: parseInt(limit), }), prisma.order.count({ where }), ]); // res.json({ // success: true, // data: { // orders, // pagination: { // page: parseInt(page), // limit: parseInt(limit), // total, // pages: Math.ceil(total / limit), // }, // }, // }); return res.status(200).json({ statusCode: 200, status: true, message: 'Orders fetched successfully', data: { orders, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / limit), }, }, }); } catch (error) { next(error); } }; // @desc Get single order exports.getOrderById = async (req, res, next) => { try { const order = await prisma.order.findFirst({ where: { id: req.params.id, userId: req.user.id }, include: { items: true, address: true }, }); if (!order) { return res.status(404).json({ // statusCode: 404, status: false, message: 'Order not found', data: null, }); } // 2️⃣ Collect productIds const productIds = order.items.map(item => item.productId); // 3️⃣ Fetch products from MongoDB const products = await Product.find({ _id: { $in: productIds }, }).select('name images'); // 4️⃣ Convert products array to map for quick lookup const productMap = {}; products.forEach(product => { productMap[product._id.toString()] = product; }); // 5️⃣ Attach product image to each order item const updatedItems = order.items.map(item => { const product = productMap[item.productId]; return { ...item, productImage: product?.images?.primary || null, productGallery: product?.images?.gallery || [], }; }); order.items = updatedItems; // res.json({ success: true, data: { order } }); return res.status(200).json({ statusCode: 200, status: true, message: 'Order fetched successfully', data: { order }, }); } catch (error) { next(error); } }; // @desc Update order status (Admin) exports.updateOrderStatus = async (req, res, next) => { try { const { status, trackingNumber } = req.body; // if (!status) // return res // .status(400) // .json({ success: false, message: 'Status is required' }); if (!status) { return res.status(400).json({ statusCode: 400, status: false, message: 'Status is required', data: null, }); } const order = await prisma.order.findUnique({ where: { id: req.params.id }, }); // if (!order) // return res // .status(404) // .json({ success: false, message: 'Order not found' }); if (!order) { return res.status(404).json({ statusCode: 404, status: false, message: 'Order not found', data: null, }); } const updateData = { status }; if (trackingNumber) updateData.trackingNumber = trackingNumber; if (status === 'SHIPPED') updateData.shippedAt = new Date(); if (status === 'DELIVERED') updateData.deliveredAt = new Date(); const updatedOrder = await prisma.order.update({ where: { id: req.params.id }, data: updateData, include: { items: true, address: true }, }); // res.json({ // success: true, // message: 'Order status updated', // data: { order: updatedOrder }, // }); return res.status(200).json({ statusCode: 200, status: true, message: 'Order status updated successfully', data: { order: updatedOrder }, }); } catch (error) { next(error); } }; // @desc Cancel order exports.cancelOrder = async (req, res, next) => { try { const order = await prisma.order.findFirst({ where: { id: req.params.id, userId: req.user.id }, }); // if (!order) // return res // .status(404) // .json({ success: false, message: 'Order not found' }); if (!order) { return res.status(404).json({ statusCode: 404, status: false, message: 'Order not found', data: null, }); } // if (!['PENDING', 'CONFIRMED'].includes(order.status)) // return res // .status(400) // .json({ success: false, message: 'Order cannot be cancelled' }); if (!['PENDING', 'CONFIRMED'].includes(order.status)) { return res.status(400).json({ statusCode: 400, status: false, message: 'Order cannot be cancelled', data: null, }); } const updatedOrder = await prisma.order.update({ where: { id: req.params.id }, data: { status: 'CANCELLED' }, include: { items: true, address: true }, }); // res.json({ // success: true, // message: 'Order cancelled', // data: { order: updatedOrder }, // }); return res.status(200).json({ statusCode: 200, status: true, message: 'Order cancelled successfully', data: { order: updatedOrder }, }); } catch (error) { next(error); } }; // exports.returnOrder = async (req, res, next) => { // try { // const order = await prisma.order.findFirst({ // where: { // id: req.params.id, // userId: req.user.id, // }, // }); // if (!order) { // return res.status(404).json({ // status: false, // message: 'Order not found', // }); // } // // ✅ RETURN STATUS CHECK (YOUR CODE GOES HERE) // if (!ALLOWED_STATUSES.includes(order.status)) { // return res.status(400).json({ // status: false, // message: 'Order not eligible for return', // }); // } // // ✅ RETURN WINDOW CHECK // const deliveredAt = order.deliveredAt || order.updatedAt; // const diffDays = // (Date.now() - new Date(deliveredAt)) / (1000 * 60 * 60 * 24); // if (diffDays > RETURN_WINDOW_DAYS) { // return res.status(400).json({ // status: false, // message: `Return allowed within ${RETURN_WINDOW_DAYS} days only`, // }); // } // // ✅ UPDATE ORDER // const updatedOrder = await prisma.order.update({ // where: { id: order.id }, // data: { // status: 'RETURN_REQUESTED', // returnRequestedAt: new Date(), // }, // }); // return res.status(200).json({ // status: true, // message: 'Return request submitted successfully', // data: { order: updatedOrder }, // }); // } catch (error) { // next(error); // } // }; // @desc Return order // @route PUT /api/orders/:id/return // @access Private exports.returnOrder = async (req, res, next) => { try { // Find the order belonging to the logged-in user const order = await prisma.order.findFirst({ where: { id: req.params.id, userId: req.user.id, }, }); if (!order) { return res.status(404).json({ status: false, message: 'Order not found', }); } // ✅ Check if order status allows return const ALLOWED_STATUSES = ['DELIVERED']; // only delivered orders can be returned if (!ALLOWED_STATUSES.includes(order.status)) { return res.status(400).json({ status: false, message: 'Order not eligible for return', }); } // ✅ Check return window (e.g., 7 days) const RETURN_WINDOW_DAYS = 7; const deliveredAt = order.deliveredAt || order.updatedAt; const diffDays = (Date.now() - new Date(deliveredAt)) / (1000 * 60 * 60 * 24); if (diffDays > RETURN_WINDOW_DAYS) { return res.status(400).json({ status: false, message: `Return allowed within ${RETURN_WINDOW_DAYS} days only`, }); } // ✅ Update order: set both status and returnStatus const updatedOrder = await prisma.order.update({ where: { id: order.id }, data: { status: 'RETURN_REQUESTED', // OrderStatus returnStatus: 'REQUESTED', // ReturnStatus (admin will use this) returnRequestedAt: new Date(), }, }); return res.status(200).json({ status: true, message: 'Return request submitted successfully', data: { order: updatedOrder }, }); } catch (error) { next(error); } }; // @desc Get all orders (Admin) exports.getAllOrdersAdmin = async (req, res, next) => { try { const { page = 1, limit = 20, status, paymentStatus } = req.query; const skip = (page - 1) * limit; const where = {}; if (status) where.status = status; if (paymentStatus) where.paymentStatus = paymentStatus; const [orders, total] = await Promise.all([ prisma.order.findMany({ where, include: { items: true, address: true, user: { select: { id: true, email: true, firstName: true, lastName: true }, }, }, orderBy: { createdAt: 'desc' }, skip: parseInt(skip), take: parseInt(limit), }), prisma.order.count({ where }), ]); // res.json({ // success: true, // data: { // orders, // pagination: { // page: parseInt(page), // limit: parseInt(limit), // total, // pages: Math.ceil(total / limit), // }, // }, // }); return res.status(200).json({ statusCode: 200, status: true, message: 'Orders fetched successfully', data: { orders, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / limit), }, }, }); } catch (error) { next(error); } }; // @desc Approve or reject a return request // @route PUT /api/orders/:id/return/status // @access Private/Admin exports.updateReturnStatus = async (req, res, next) => { try { const { id } = req.params; const { action } = req.body; // "APPROVE" or "REJECT" const order = await prisma.order.findUnique({ where: { id } }); if (!order) { return res .status(404) .json({ status: false, message: 'Order not found' }); } if (order.returnStatus !== 'REQUESTED') { return res .status(400) .json({ status: false, message: 'No return request pending' }); } let newStatus; if (action === 'APPROVE') newStatus = 'APPROVED'; else if (action === 'REJECT') newStatus = 'REJECTED'; else return res.status(400).json({ status: false, message: 'Invalid action' }); const updatedOrder = await prisma.order.update({ where: { id }, data: { returnStatus: newStatus }, }); res.status(200).json({ status: true, message: `Return request ${action.toLowerCase()}ed successfully`, data: { order: updatedOrder }, }); } catch (error) { next(error); } }; // @desc Get all return requests (Admin only) // @route GET /api/orders/admin/returns // @access Private/Admin exports.getAdminReturnRequests = async (req, res) => { try { const page = Number(req.query.page) || 1; const limit = Number(req.query.limit) || 10; const skip = (page - 1) * limit; const [returns, count] = await Promise.all([ prisma.order.findMany({ where: { returnStatus: { in: ['REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'], }, }, include: { user: true, address: true, items: true, }, orderBy: { returnRequestedAt: 'desc' }, skip, take: limit, }), prisma.order.count({ where: { returnStatus: { in: ['REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'], }, }, }), ]); res.json({ status: true, count, data: returns, }); } catch (error) { console.error('Admin return list error:', error); res.status(500).json({ status: false, message: 'Failed to fetch return requests', }); } }; // @desc Get all returned products (Admin only) // @route GET /api/orders/admin/returns/list // @access Private/Admin exports.getReturnedProducts = async (req, res, next) => { try { const returnedOrders = await prisma.order.findMany({ where: { returnStatus: { in: ['APPROVED', 'COMPLETED'] }, }, include: { user: true, address: true, items: true, }, orderBy: { returnRequestedAt: 'desc', }, }); res.status(200).json({ status: true, count: returnedOrders.length, data: returnedOrders, }); } catch (error) { next(error); } }; exports.getReturnRequestById = async (req, res) => { try { const { id } = req.params; // Fetch the order with related user, address, and items const order = await prisma.order.findUnique({ where: { id }, include: { user: true, address: true, items: true, }, }); if (!order) { return res .status(404) .json({ status: false, message: 'Return request not found' }); } // Ensure this order is a return request if ( !['RETURN_REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'].includes( order.returnStatus ) ) { return res .status(400) .json({ status: false, message: 'This order is not a return request' }); } res.json({ status: true, data: order }); } catch (error) { console.error('Error fetching return request:', error); res.status(500).json({ status: false, message: 'Server error' }); } }; // module.exports = { getReturnRequestById };