720 lines
19 KiB
JavaScript
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;
|
|
}
|