// controllers/orderTrackingController.js const { prisma } = require('../config/database'); const { calculateDeliveryDate, getDeliveryEstimation, } = require('../services/deliveryEstimationService'); /** * @desc Get order tracking details * @route GET /api/orders/:orderId/tracking * @access Private */ exports.getOrderTracking = async (req, res, next) => { try { const { orderId } = req.params; const userId = req.user.id; // Get order with all details const order = await prisma.order.findUnique({ where: { id: orderId }, include: { items: true, address: true, user: { select: { id: true, email: true, firstName: true, lastName: true, phone: true, }, }, }, }); if (!order) { return res.status(404).json({ success: false, message: 'Order not found', }); } // Check if user owns this order if (order.userId !== userId) { return res.status(403).json({ success: false, message: 'Unauthorized to view this order', }); } // Calculate delivery estimation const deliveryEstimation = await getDeliveryEstimation( order.address.postalCode, 'STANDARD' ); // Get order timeline/history const timeline = generateOrderTimeline(order, deliveryEstimation.data); // Get current status details const statusDetails = getStatusDetails(order); res.status(200).json({ success: true, data: { order: { id: order.id, orderNumber: order.orderNumber, status: order.status, paymentStatus: order.paymentStatus, totalAmount: parseFloat(order.totalAmount), createdAt: order.createdAt, }, tracking: { currentStatus: statusDetails, timeline, deliveryEstimation: deliveryEstimation.data, trackingNumber: order.trackingNumber, }, shippingAddress: { name: `${order.address.firstName} ${order.address.lastName}`, addressLine1: order.address.addressLine1, addressLine2: order.address.addressLine2, city: order.address.city, state: order.address.state, postalCode: order.address.postalCode, country: order.address.country, phone: order.address.phone, }, items: order.items.map(item => ({ productName: item.productName, productSku: item.productSku, quantity: item.quantity, price: parseFloat(item.price), })), }, }); } catch (error) { console.error('Get order tracking error:', error); next(error); } }; /** * @desc Update order status (Admin) - FLEXIBLE VERSION * @route PUT /api/admin/orders/:orderId/status * @access Private/Admin */ // exports.updateOrderStatus = async (req, res, next) => { // try { // const { orderId } = req.params; // const { status, trackingNumber, notes, skipValidation = false } = req.body; // console.log('📦 Updating order status:', { // orderId, // currentStatus: 'Fetching...', // newStatus: status, // trackingNumber, // }); // const order = await prisma.order.findUnique({ // where: { id: orderId }, // }); // if (!order) { // return res.status(404).json({ // success: false, // message: 'Order not found', // }); // } // console.log('📦 Current order status:', order.status); // // ✅ FLEXIBLE VALIDATION - Allow skipping intermediate steps // const validTransitions = { // PENDING: ['CONFIRMED', 'PROCESSING', 'SHIPPED', 'CANCELLED'], // CONFIRMED: ['PROCESSING', 'SHIPPED', 'CANCELLED'], // PROCESSING: ['SHIPPED', 'DELIVERED', 'CANCELLED'], // SHIPPED: ['DELIVERED'], // DELIVERED: ['RETURN_REQUESTED'], // RETURN_REQUESTED: ['REFUNDED'], // CANCELLED: [], // Cannot transition from cancelled // REFUNDED: [], // Cannot transition from refunded // }; // // Validate transition (unless skipValidation is true) // if (!skipValidation && !validTransitions[order.status]?.includes(status)) { // return res.status(400).json({ // success: false, // message: `Cannot transition from ${order.status} to ${status}`, // allowedTransitions: validTransitions[order.status], // hint: 'You can set skipValidation: true to force this transition', // }); // } // // ✅ Auto-update intermediate statuses if needed // let intermediateUpdates = []; // if (order.status === 'PENDING' && status === 'SHIPPED') { // intermediateUpdates = ['CONFIRMED', 'PROCESSING']; // console.log( // '⚡ Auto-updating intermediate statuses:', // intermediateUpdates // ); // } else if (order.status === 'CONFIRMED' && status === 'DELIVERED') { // intermediateUpdates = ['PROCESSING', 'SHIPPED']; // console.log( // '⚡ Auto-updating intermediate statuses:', // intermediateUpdates // ); // } else if (order.status === 'PENDING' && status === 'DELIVERED') { // intermediateUpdates = ['CONFIRMED', 'PROCESSING', 'SHIPPED']; // console.log( // '⚡ Auto-updating intermediate statuses:', // intermediateUpdates // ); // } // // Build update data // const updateData = { status }; // if (trackingNumber) { // updateData.trackingNumber = trackingNumber; // } // if (status === 'SHIPPED' && !order.shippedAt) { // updateData.shippedAt = new Date(); // } // if (status === 'DELIVERED' && !order.deliveredAt) { // updateData.deliveredAt = new Date(); // updateData.shippedAt = updateData.shippedAt || new Date(); // Ensure shipped date is set // } // // Update order // const updatedOrder = await prisma.order.update({ // where: { id: orderId }, // data: updateData, // }); // console.log('✅ Order status updated:', { // from: order.status, // to: updatedOrder.status, // trackingNumber: updatedOrder.trackingNumber, // }); // // TODO: Send notification to user // // await sendOrderStatusNotification(updatedOrder); // res.status(200).json({ // success: true, // message: `Order status updated to ${status}`, // data: updatedOrder, // intermediateUpdates: // intermediateUpdates.length > 0 ? intermediateUpdates : undefined, // }); // } catch (error) { // console.error('❌ Update order status error:', error); // next(error); // } // }; /** * @desc Update order status (Admin) - ALL MANUAL * @route PUT /api/admin/orders/:orderId/status * @access Private/Admin */ // exports.updateOrderStatus = async (req, res, next) => { // try { // const { orderId } = req.params; // const { status, trackingNumber, notes } = req.body; // console.log('📦 Updating order status:', { // orderId, // newStatus: status, // }); // const order = await prisma.order.findUnique({ // where: { id: orderId }, // }); // if (!order) { // return res.status(404).json({ // success: false, // message: 'Order not found', // }); // } // console.log('📦 Current order status:', order.status); // // ✅ SIMPLE VALIDATION - Admin can update to any status // const validStatuses = [ // 'PENDING', // 'CONFIRMED', // 'PROCESSING', // 'SHIPPED', // 'DELIVERED', // 'CANCELLED', // 'RETURN_REQUESTED', // ]; // if (!validStatuses.includes(status)) { // return res.status(400).json({ // success: false, // message: `Invalid status: ${status}`, // validStatuses, // }); // } // // Build update data // const updateData = { status }; // if (trackingNumber) { // updateData.trackingNumber = trackingNumber; // } // // Auto-set timestamps // if (status === 'SHIPPED' && !order.shippedAt) { // updateData.shippedAt = new Date(); // } // if (status === 'DELIVERED' && !order.deliveredAt) { // updateData.deliveredAt = new Date(); // } // // Update order // const updatedOrder = await prisma.order.update({ // where: { id: orderId }, // data: updateData, // }); // console.log('✅ Order status updated:', { // from: order.status, // to: updatedOrder.status, // }); // res.status(200).json({ // success: true, // message: `Order status updated to ${status}`, // data: updatedOrder, // }); // } catch (error) { // console.error('❌ Update order status error:', error); // next(error); // } // }; exports.updateOrderStatus = async (req, res, next) => { try { const { orderId } = req.params; const { status, trackingNumber, notes } = req.body; const adminId = req.user.id; const ipAddress = req.ip || req.connection.remoteAddress; const userAgent = req.get("user-agent"); console.log("📦 Updating order status:", { orderId, newStatus: status, admin: adminId, }); const order = await prisma.order.findUnique({ where: { id: orderId }, }); if (!order) { return res.status(404).json({ success: false, message: "Order not found", }); } const oldStatus = order.status; const validStatuses = [ "PENDING", "CONFIRMED", "PROCESSING", "SHIPPED", "DELIVERED", "CANCELLED", "RETURN_REQUESTED", ]; if (!validStatuses.includes(status)) { return res.status(400).json({ success: false, message: `Invalid status: ${status}`, validStatuses, }); } const updateData = { status }; if (trackingNumber) updateData.trackingNumber = trackingNumber; if (status === "SHIPPED" && !order.shippedAt) { updateData.shippedAt = new Date(); } if (status === "DELIVERED" && !order.deliveredAt) { updateData.deliveredAt = new Date(); } // ✅ SINGLE CLEAN TRANSACTION const result = await prisma.$transaction(async (tx) => { const updatedOrder = await tx.order.update({ where: { id: orderId }, data: updateData, }); const historyRecord = await tx.orderStatusHistory.create({ data: { orderId, fromStatus: oldStatus, toStatus: status, changedBy: adminId, trackingNumber: trackingNumber || null, notes: notes || null, ipAddress, userAgent, }, }); return { order: updatedOrder, history: historyRecord }; }); // ✅ Auto stock reduction AFTER successful transaction let stockReduction = null; if (status === "DELIVERED" && oldStatus !== "DELIVERED") { try { stockReduction = await reduceStockOnDelivery(orderId); console.log("✅ Stock reduced automatically"); } catch (err) { console.error("❌ Stock reduction failed:", err); } } console.log("✅ Order status updated:", { from: oldStatus, to: result.order.status, }); res.status(200).json({ success: true, message: `Order status updated from ${oldStatus} to ${status}`, data: result.order, stockReduction, historyId: result.history.id, }); } catch (error) { console.error("❌ Update order status error:", error); next(error); } }; /** * @desc Get order status history * @route GET /api/admin/orders/:orderId/history * @access Private/Admin */ exports.getOrderStatusHistory = async (req, res, next) => { try { const { orderId } = req.params; const history = await prisma.orderStatusHistory.findMany({ where: { orderId }, include: { admin: { select: { id: true, email: true, firstName: true, lastName: true, }, }, }, orderBy: { createdAt: 'desc' }, }); res.status(200).json({ success: true, data: history, }); } catch (error) { console.error('❌ Get status history error:', error); next(error); } }; /** * @desc Bulk update order status (Admin) * @route PUT /api/admin/orders/bulk-status * @access Private/Admin */ exports.bulkUpdateOrderStatus = async (req, res, next) => { try { const { orderIds, status, trackingNumbers } = req.body; if (!orderIds || !Array.isArray(orderIds) || orderIds.length === 0) { return res.status(400).json({ success: false, message: 'Order IDs array is required', }); } const updateData = { status }; if (status === 'SHIPPED') { updateData.shippedAt = new Date(); } if (status === 'DELIVERED') { updateData.deliveredAt = new Date(); } // Update all orders const updates = await Promise.all( orderIds.map(async (orderId, index) => { const data = { ...updateData }; if (trackingNumbers && trackingNumbers[index]) { data.trackingNumber = trackingNumbers[index]; } return prisma.order.update({ where: { id: orderId }, data, }); }) ); res.status(200).json({ success: true, message: `${updates.length} orders updated to ${status}`, data: updates, }); } catch (error) { console.error('Bulk update error:', error); next(error); } }; /** * @desc Get delivery estimation for pincode * @route POST /api/delivery/estimate * @access Public */ exports.getDeliveryEstimate = async (req, res, next) => { try { const { pincode, shippingMethod = 'STANDARD' } = req.body; if (!pincode) { return res.status(400).json({ success: false, message: 'Pincode is required', }); } const estimation = await getDeliveryEstimation(pincode, shippingMethod); res.status(200).json(estimation); } catch (error) { console.error('Get delivery estimate error:', error); next(error); } }; // ========================================== // HELPER FUNCTIONS // ========================================== /** * Generate order timeline */ function generateOrderTimeline(order, deliveryEstimation) { const timeline = []; timeline.push({ status: 'PLACED', title: 'Order Placed', description: 'Your order has been placed successfully', timestamp: order.createdAt, completed: true, icon: '🛒', }); if (order.status !== 'PENDING') { timeline.push({ status: 'CONFIRMED', title: 'Order Confirmed', description: 'Your order has been confirmed', timestamp: order.createdAt, completed: true, icon: '✅', }); } else { timeline.push({ status: 'CONFIRMED', title: 'Order Confirmation', description: 'Awaiting order confirmation', completed: false, icon: '⏳', }); } if (['PROCESSING', 'SHIPPED', 'DELIVERED'].includes(order.status)) { timeline.push({ status: 'PROCESSING', title: 'Processing', description: 'Your order is being processed', timestamp: order.createdAt, completed: true, icon: '📦', }); } else if (order.status === 'CONFIRMED') { timeline.push({ status: 'PROCESSING', title: 'Processing', description: 'Order will be processed soon', completed: false, icon: '⏳', }); } if (['SHIPPED', 'DELIVERED'].includes(order.status)) { timeline.push({ status: 'SHIPPED', title: 'Shipped', description: order.trackingNumber ? `Tracking: ${order.trackingNumber}` : 'Your order has been shipped', timestamp: order.shippedAt || new Date(), completed: true, icon: '🚚', }); } else if (['CONFIRMED', 'PROCESSING'].includes(order.status)) { timeline.push({ status: 'SHIPPED', title: 'Shipping', description: 'Your order will be shipped soon', completed: false, icon: '⏳', }); } if (order.status === 'DELIVERED') { timeline.push({ status: 'OUT_FOR_DELIVERY', title: 'Out for Delivery', description: 'Your order is out for delivery', timestamp: order.deliveredAt || new Date(), completed: true, icon: '🛵', }); } else if (order.status === 'SHIPPED') { timeline.push({ status: 'OUT_FOR_DELIVERY', title: 'Out for Delivery', description: 'Will be out for delivery soon', completed: false, icon: '⏳', }); } if (order.status === 'DELIVERED') { timeline.push({ status: 'DELIVERED', title: 'Delivered', description: 'Your order has been delivered', timestamp: order.deliveredAt, completed: true, icon: '🎉', }); } else { const estimatedDate = deliveryEstimation?.estimatedDelivery?.formatted; timeline.push({ status: 'DELIVERED', title: 'Delivery', description: estimatedDate ? `Expected by ${estimatedDate}` : 'Estimated delivery date will be updated', completed: false, icon: '📍', }); } return timeline; } /** * Get current status details */ function getStatusDetails(order) { const statusMap = { PENDING: { label: 'Order Pending', description: 'Your order is awaiting confirmation', color: 'yellow', progress: 10, }, CONFIRMED: { label: 'Order Confirmed', description: 'Your order has been confirmed and will be processed soon', color: 'blue', progress: 25, }, PROCESSING: { label: 'Processing', description: 'Your order is being prepared for shipment', color: 'blue', progress: 50, }, SHIPPED: { label: 'Shipped', description: 'Your order is on the way', color: 'purple', progress: 75, }, DELIVERED: { label: 'Delivered', description: 'Your order has been delivered', color: 'green', progress: 100, }, CANCELLED: { label: 'Cancelled', description: 'Your order has been cancelled', color: 'red', progress: 0, }, RETURN_REQUESTED: { label: 'Return Requested', description: 'Return request is being processed', color: 'orange', progress: 100, }, REFUNDED: { label: 'Refunded', description: 'Your order has been refunded', color: 'green', progress: 100, }, }; return statusMap[order.status] || statusMap.PENDING; }