first commit

This commit is contained in:
tusuii
2026-02-19 17:25:38 +05:30
commit 09ea6d4efb
72 changed files with 24296 additions and 0 deletions

View File

@@ -0,0 +1,822 @@
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 };