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

720 lines
19 KiB
JavaScript

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