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

823 lines
22 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };