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

57
src/config/database.js Normal file
View File

@@ -0,0 +1,57 @@
const { PrismaClient } = require('@prisma/client');
const mongoose = require('mongoose');
// PostgreSQL Connection (Prisma)
const prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'],
});
// MongoDB Connection (Mongoose)
const connectMongoDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log('✅ MongoDB connected successfully');
} catch (error) {
console.error('❌ MongoDB connection error:', error);
process.exit(1);
}
};
// PostgreSQL Connection (Prisma)
const connectPostgreSQL = async () => {
try {
await prisma.$connect();
console.log('✅ PostgreSQL connected successfully');
} catch (error) {
console.error('❌ PostgreSQL connection error:', error);
process.exit(1);
}
};
// Initialize all database connections
const initializeDatabases = async () => {
await Promise.all([
connectPostgreSQL(),
connectMongoDB(),
]);
};
// Graceful shutdown
const closeDatabaseConnections = async () => {
try {
await Promise.all([
prisma.$disconnect(),
mongoose.connection.close(),
]);
console.log('✅ Database connections closed');
} catch (error) {
console.error('❌ Error closing database connections:', error);
}
};
module.exports = {
prisma,
mongoose,
initializeDatabases,
closeDatabaseConnections,
};

View File

@@ -0,0 +1,5 @@
// backend/config/returnPolicy.js
module.exports = {
RETURN_WINDOW_DAYS: 7,
ALLOWED_STATUSES: ['DELIVERED'],
};

16
src/config/s3.js Normal file
View File

@@ -0,0 +1,16 @@
// config/s3.js
// const { S3Client } from "@aws-sdk/client-s3";
const { S3Client } = require("@aws-sdk/client-s3");
const s3 = new S3Client({
endpoint: "https://s3.sahasrarameta.tech",
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
forcePathStyle: true, // IMPORTANT for MinIO
});
// export default s3;
module.exports = s3;

View File

@@ -0,0 +1,260 @@
const { prisma } = require('../../config/database');
exports.getAllCategories = async (req, res, next) => {
try {
const categories = await prisma.category.findMany({
orderBy: { name: 'asc' },
});
// res.json({ success: true, data: categories });
res.status(200).json({
// statusCode: 200,
status: true,
message: 'Categories fetched successfully',
data: categories,
});
} catch (error) {
next(error);
}
};
exports.createCategory = async (req, res, next) => {
try {
const { name, description, image, parentId, metaTitle, metaDescription } =
req.body;
// Generate slug from name
let slug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
// Check if slug already exists with the same parent
const existing = await prisma.category.findFirst({
where: {
slug,
parentId: parentId || null, // Handle both subcategories and root categories
},
});
if (existing) {
// If exists under same parent, append timestamp to make it unique
slug = `${slug}-${Date.now()}`;
}
const category = await prisma.category.create({
data: {
name,
slug,
description,
image,
parentId,
metaTitle,
metaDescription,
},
});
res.status(201).json({
statusCode: 201,
status: true,
message: 'Category created successfully',
data: category,
});
} catch (error) {
// Handle Prisma duplicate error explicitly
if (error.code === 'P2002') {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Duplicate field value entered',
});
}
next(error);
}
};
exports.updateCategory = async (req, res, next) => {
try {
const { id } = req.params;
const { name, description, image, parentId, metaTitle, metaDescription } =
req.body;
let slug;
if (name) {
slug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
const category = await prisma.category.update({
where: { id },
data: {
name,
slug,
description,
image,
parentId,
metaTitle,
metaDescription,
},
});
res.json({
status: true,
message: 'Category updated successfully',
data: category,
});
} catch (error) {
next(error);
}
};
exports.deleteCategory = async (req, res, next) => {
try {
const { id } = req.params;
await prisma.category.delete({
where: { id },
});
res.json({
status: true,
message: 'Category deleted successfully',
});
} catch (error) {
next(error);
}
};
exports.toggleCategoryStatus = async (req, res, next) => {
try {
const { id } = req.params;
const { isActive } = req.body;
// 1⃣ Update parent category
const parentCategory = await prisma.category.update({
where: { id },
data: { isActive },
});
// 2⃣ If parent is being deactivated, also deactivate all children recursively
if (!isActive) {
const deactivateChildren = async parentId => {
const children = await prisma.category.findMany({
where: { parentId },
});
for (const child of children) {
await prisma.category.update({
where: { id: child.id },
data: { isActive: false },
});
// Recursive call for nested subcategories
await deactivateChildren(child.id);
}
};
await deactivateChildren(id);
}
res.json({
status: true,
message: `Category ${isActive ? 'activated' : 'deactivated'} successfully`,
data: parentCategory,
});
} catch (error) {
next(error);
}
};
exports.reorderCategories = async (req, res) => {
const { orders } = req.body;
await Promise.all(
orders.map(item =>
prisma.category.update({
where: { id: item.id },
data: { sequence: item.sequence },
})
)
);
res.json({
status: true,
message: 'Category order updated',
});
};
exports.getCategoryHierarchy = async (req, res, next) => {
try {
// 1. Fetch all categories
const categories = await prisma.category.findMany({
orderBy: { name: 'asc' },
});
// 2. Convert array to a lookup map
const lookup = {};
categories.forEach(cat => {
lookup[cat.id] = { ...cat, children: [] };
});
const hierarchy = [];
// 3. Build hierarchical structure
categories.forEach(cat => {
if (cat.parentId) {
lookup[cat.parentId].children.push(lookup[cat.id]);
} else {
hierarchy.push(lookup[cat.id]);
}
});
res.status(200).json({
// statusCode: 200,
status: true,
message: 'Category hierarchy fetched successfully',
data: hierarchy,
});
} catch (error) {
next(error);
}
};
exports.getCategoryById = async (req, res) => {
try {
const { id } = req.params;
if (!id) {
return res.status(400).json({
success: false,
message: 'Category ID is required',
});
}
const category = await prisma.category.findUnique({
where: {
id: id, // ✅ PASS THE ACTUAL STRING VALUE
},
});
if (!category) {
return res.status(404).json({
success: false,
message: 'Category not found',
});
}
return res.status(200).json({
success: true,
message: 'Category details fetched successfully',
data: category,
});
} catch (error) {
console.error('Get category by id error:', error);
return res.status(500).json({
success: false,
message: 'Error fetching category',
});
}
};

View File

@@ -0,0 +1,463 @@
// const { prisma } = require('../../config/database');
// exports.getAllCoupons = async (req, res, next) => {
// try {
// const coupons = await prisma.coupon.findMany({
// orderBy: { createdAt: 'desc' },
// });
// // res.json({ success: true, data: coupons });
// res.status(200).json({
// // statusCode: 200,
// status: true,
// message: 'Coupons fetched successfully',
// data: coupons,
// });
// } catch (error) {
// next(error);
// }
// };
// exports.createCoupon = async (req, res, next) => {
// try {
// const {
// code,
// description,
// type,
// value,
// minOrderAmount,
// maxUses,
// validFrom,
// validUntil,
// } = req.body;
// const coupon = await prisma.coupon.create({
// data: {
// code,
// description,
// type,
// value,
// minOrderAmount,
// maxUses,
// validFrom: new Date(validFrom),
// validUntil: new Date(validUntil),
// },
// });
// res.status(201).json({
// // statusCode: 201,
// status: true,
// message: 'Coupon created successfully',
// data: coupon,
// });
// } catch (error) {
// next(error);
// }
// };
// controllers/admin/couponController.js - ENHANCED VERSION
const { prisma } = require('../../config/database');
// ==========================================
// ADMIN COUPON MANAGEMENT
// ==========================================
/**
* @desc Get all coupons (with filters)
* @route GET /api/admin/coupons
* @access Private/Admin
*/
exports.getAllCoupons = async (req, res, next) => {
try {
const { isActive, type, search, page = 1, limit = 20 } = req.query;
const skip = (parseInt(page) - 1) * parseInt(limit);
// Build filter
const where = {};
if (isActive !== undefined) {
where.isActive = isActive === 'true';
}
if (type) {
where.type = type;
}
if (search) {
where.OR = [
{ code: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
];
}
const [coupons, total] = await Promise.all([
prisma.coupon.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: parseInt(limit),
}),
prisma.coupon.count({ where }),
]);
// Add usage statistics
const couponsWithStats = coupons.map(coupon => ({
...coupon,
usagePercentage: coupon.maxUses
? Math.round((coupon.usedCount / coupon.maxUses) * 100)
: 0,
isExpired: new Date() > new Date(coupon.validUntil),
isNotStarted: new Date() < new Date(coupon.validFrom),
remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null,
}));
res.status(200).json({
status: true,
message: 'Coupons fetched successfully',
data: {
coupons: couponsWithStats,
pagination: {
total,
page: parseInt(page),
limit: parseInt(limit),
pages: Math.ceil(total / parseInt(limit)),
},
},
});
} catch (error) {
next(error);
}
};
/**
* @desc Get single coupon by ID
* @route GET /api/admin/coupons/:id
* @access Private/Admin
*/
exports.getCouponById = async (req, res, next) => {
try {
const { id } = req.params;
const coupon = await prisma.coupon.findUnique({
where: { id },
});
if (!coupon) {
return res.status(404).json({
status: false,
message: 'Coupon not found',
});
}
// Get usage statistics
const usageStats = {
...coupon,
usagePercentage: coupon.maxUses
? Math.round((coupon.usedCount / coupon.maxUses) * 100)
: 0,
isExpired: new Date() > new Date(coupon.validUntil),
isNotStarted: new Date() < new Date(coupon.validFrom),
remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null,
};
res.status(200).json({
status: true,
message: 'Coupon fetched successfully',
data: usageStats,
});
} catch (error) {
next(error);
}
};
/**
* @desc Create new coupon
* @route POST /api/admin/coupons
* @access Private/Admin
*/
exports.createCoupon = async (req, res, next) => {
try {
const {
code,
description,
type,
value,
minOrderAmount,
maxUses,
validFrom,
validUntil,
isActive = true,
} = req.body;
// Validate code uniqueness
const existingCoupon = await prisma.coupon.findUnique({
where: { code: code.toUpperCase() },
});
if (existingCoupon) {
return res.status(400).json({
status: false,
message: 'Coupon code already exists',
});
}
// Validate dates
const fromDate = new Date(validFrom);
const untilDate = new Date(validUntil);
if (fromDate >= untilDate) {
return res.status(400).json({
status: false,
message: 'Valid until date must be after valid from date',
});
}
// Validate value based on type
if (type === 'PERCENTAGE' && (value < 0 || value > 100)) {
return res.status(400).json({
status: false,
message: 'Percentage value must be between 0 and 100',
});
}
if (type === 'FIXED_AMOUNT' && value < 0) {
return res.status(400).json({
status: false,
message: 'Fixed amount must be greater than 0',
});
}
const coupon = await prisma.coupon.create({
data: {
code: code.toUpperCase(),
description,
type,
value: parseFloat(value),
minOrderAmount: minOrderAmount ? parseFloat(minOrderAmount) : null,
maxUses: maxUses ? parseInt(maxUses) : null,
validFrom: fromDate,
validUntil: untilDate,
isActive,
},
});
res.status(201).json({
status: true,
message: 'Coupon created successfully',
data: coupon,
});
} catch (error) {
console.error('Create coupon error:', error);
next(error);
}
};
/**
* @desc Update coupon
* @route PUT /api/admin/coupons/:id
* @access Private/Admin
*/
exports.updateCoupon = async (req, res, next) => {
try {
const { id } = req.params;
const {
code,
description,
type,
value,
minOrderAmount,
maxUses,
validFrom,
validUntil,
isActive,
} = req.body;
// Check if coupon exists
const existingCoupon = await prisma.coupon.findUnique({
where: { id },
});
if (!existingCoupon) {
return res.status(404).json({
status: false,
message: 'Coupon not found',
});
}
// If code is being changed, check uniqueness
if (code && code.toUpperCase() !== existingCoupon.code) {
const duplicateCoupon = await prisma.coupon.findUnique({
where: { code: code.toUpperCase() },
});
if (duplicateCoupon) {
return res.status(400).json({
status: false,
message: 'Coupon code already exists',
});
}
}
// Validate dates if provided
const fromDate = validFrom ? new Date(validFrom) : existingCoupon.validFrom;
const untilDate = validUntil
? new Date(validUntil)
: existingCoupon.validUntil;
if (fromDate >= untilDate) {
return res.status(400).json({
status: false,
message: 'Valid until date must be after valid from date',
});
}
const updateData = {};
if (code) updateData.code = code.toUpperCase();
if (description !== undefined) updateData.description = description;
if (type) updateData.type = type;
if (value !== undefined) updateData.value = parseFloat(value);
if (minOrderAmount !== undefined)
updateData.minOrderAmount = minOrderAmount
? parseFloat(minOrderAmount)
: null;
if (maxUses !== undefined)
updateData.maxUses = maxUses ? parseInt(maxUses) : null;
if (validFrom) updateData.validFrom = fromDate;
if (validUntil) updateData.validUntil = untilDate;
if (isActive !== undefined) updateData.isActive = isActive;
const coupon = await prisma.coupon.update({
where: { id },
data: updateData,
});
res.status(200).json({
status: true,
message: 'Coupon updated successfully',
data: coupon,
});
} catch (error) {
next(error);
}
};
/**
* @desc Delete coupon
* @route DELETE /api/admin/coupons/:id
* @access Private/Admin
*/
exports.deleteCoupon = async (req, res, next) => {
try {
const { id } = req.params;
const coupon = await prisma.coupon.findUnique({
where: { id },
});
if (!coupon) {
return res.status(404).json({
status: false,
message: 'Coupon not found',
});
}
// Check if coupon has been used
if (coupon.usedCount > 0) {
return res.status(400).json({
status: false,
message:
'Cannot delete a coupon that has been used. Consider deactivating it instead.',
});
}
await prisma.coupon.delete({
where: { id },
});
res.status(200).json({
status: true,
message: 'Coupon deleted successfully',
});
} catch (error) {
next(error);
}
};
/**
* @desc Toggle coupon active status
* @route PATCH /api/admin/coupons/:id/toggle
* @access Private/Admin
*/
exports.toggleCouponStatus = async (req, res, next) => {
try {
const { id } = req.params;
const coupon = await prisma.coupon.findUnique({
where: { id },
});
if (!coupon) {
return res.status(404).json({
status: false,
message: 'Coupon not found',
});
}
const updatedCoupon = await prisma.coupon.update({
where: { id },
data: { isActive: !coupon.isActive },
});
res.status(200).json({
status: true,
message: `Coupon ${updatedCoupon.isActive ? 'activated' : 'deactivated'} successfully`,
data: updatedCoupon,
});
} catch (error) {
next(error);
}
};
/**
* @desc Get coupon statistics
* @route GET /api/admin/coupons/stats/overview
* @access Private/Admin
*/
exports.getCouponStats = async (req, res, next) => {
try {
const [totalCoupons, activeCoupons, expiredCoupons, totalRedemptions] =
await Promise.all([
prisma.coupon.count(),
prisma.coupon.count({ where: { isActive: true } }),
prisma.coupon.count({
where: {
validUntil: { lt: new Date() },
},
}),
prisma.coupon.aggregate({
_sum: { usedCount: true },
}),
]);
// Get most used coupons
const mostUsed = await prisma.coupon.findMany({
where: { usedCount: { gt: 0 } },
orderBy: { usedCount: 'desc' },
take: 5,
});
res.status(200).json({
status: true,
message: 'Coupon statistics fetched successfully',
data: {
totalCoupons,
activeCoupons,
expiredCoupons,
totalRedemptions: totalRedemptions._sum.usedCount || 0,
mostUsedCoupons: mostUsed,
},
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,826 @@
// const { prisma } = require('../../config/database');
// const Product = require('../../models/mongodb/Product');
// exports.getDashboardStats = async (req, res, next) => {
// try {
// const [
// totalUsers,
// totalOrders,
// totalProducts,
// totalRevenue,
// recentOrders,
// topProducts,
// ] = await Promise.all([
// prisma.user.count(),
// prisma.order.count(),
// Product.countDocuments(),
// prisma.order.aggregate({
// _sum: { totalAmount: true },
// where: { paymentStatus: 'PAID' },
// }),
// prisma.order.findMany({
// take: 5,
// orderBy: { createdAt: 'desc' },
// include: {
// user: {
// select: { id: true, email: true, firstName: true, lastName: true },
// },
// items: true,
// },
// }),
// Product.find({ status: 'active' })
// .sort({ purchaseCount: -1 })
// .limit(5)
// .lean()
// .then(products =>
// products.map(product => {
// // ✅ Fixed: Check gallery first since that's where your images are
// let image =
// (product.images?.gallery && product.images.gallery.length > 0
// ? product.images.gallery[0]
// : null) ||
// product.images?.primary ||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
// ? product.variants[0].images[0]
// : null) ||
// 'https://via.placeholder.com/300';
// return {
// ...product,
// displayImage: image,
// };
// })
// ),
// ]);
// // ✅ NEW: Fetch product details for recent orders
// const orderProductIds = recentOrders
// .flatMap(order => order.items.map(item => item.productId))
// .filter(Boolean);
// const orderProducts = await Product.find({
// _id: { $in: orderProductIds }
// }).lean();
// // ✅ Create a map of productId -> product for quick lookup
// const productMap = {};
// orderProducts.forEach(product => {
// const image =
// (product.images?.gallery && product.images.gallery.length > 0
// ? product.images.gallery[0]
// : null) ||
// product.images?.primary ||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
// ? product.variants[0].images[0]
// : null) ||
// 'https://via.placeholder.com/300';
// productMap[product._id.toString()] = {
// ...product,
// displayImage: image,
// };
// });
// // ✅ Enhance recent orders with product images
// const enhancedRecentOrders = recentOrders.map(order => ({
// ...order,
// items: order.items.map(item => ({
// ...item,
// productImage: productMap[item.productId]?.displayImage || 'https://via.placeholder.com/300',
// productDetails: productMap[item.productId] || null,
// })),
// }));
// res.status(200).json({
// statusCode: 200,
// status: true,
// message: 'Dashboard stats fetched successfully',
// data: {
// totalUsers,
// totalOrders,
// totalProducts,
// totalRevenue: totalRevenue._sum.totalAmount || 0,
// recentOrders: enhancedRecentOrders,
// topProducts,
// },
// });
// } catch (error) {
// next(error);
// }
// };
// controllers/admin/dashboardController.js - WITH ORDER OVERVIEW GRAPHS
const { prisma } = require('../../config/database');
const Product = require('../../models/mongodb/Product');
const { getLowStockProducts, getInventoryStats } = require('../../services/inventoryService');
/**
* @desc Get dashboard stats with order overview graphs
* @route GET /api/admin/dashboard/stats
* @access Private/Admin
*/
// exports.getDashboardStats = async (req, res, next) => {
// try {
// const [
// totalUsers,
// totalOrders,
// totalProducts,
// totalRevenue,
// recentOrders,
// topProducts,
// orderOverview,
// revenueOverview,
// ordersByStatus,
// monthlyComparison,
// ] = await Promise.all([
// // Total Users
// prisma.user.count(),
// // Total Orders
// prisma.order.count(),
// // Total Products
// Product.countDocuments(),
// // Total Revenue
// prisma.order.aggregate({
// _sum: { totalAmount: true },
// where: { paymentStatus: 'PAID' },
// }),
// // Recent Orders
// prisma.order.findMany({
// take: 5,
// orderBy: { createdAt: 'desc' },
// include: {
// user: {
// select: { id: true, email: true, firstName: true, lastName: true },
// },
// items: true,
// },
// }),
// // Top Products
// Product.find({ status: 'active' })
// .sort({ purchaseCount: -1 })
// .limit(5)
// .lean()
// .then(products =>
// products.map(product => {
// let image =
// (product.images?.gallery && product.images.gallery.length > 0
// ? product.images.gallery[0]
// : null) ||
// product.images?.primary ||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
// ? product.variants[0].images[0]
// : null) ||
// 'https://via.placeholder.com/300';
// return {
// ...product,
// displayImage: image,
// };
// })
// ),
// // ✅ Order Overview (Last 30 Days)
// getOrderOverview(),
// // ✅ Revenue Overview (Last 30 Days)
// getRevenueOverview(),
// // ✅ Orders by Status
// getOrdersByStatus(),
// // ✅ Monthly Comparison (Current vs Previous Month)
// getMonthlyComparison(),
// ]);
// // Enhance recent orders with product images
// const orderProductIds = recentOrders
// .flatMap(order => order.items.map(item => item.productId))
// .filter(Boolean);
// const orderProducts = await Product.find({
// _id: { $in: orderProductIds }
// }).lean();
// const productMap = {};
// orderProducts.forEach(product => {
// const image =
// (product.images?.gallery && product.images.gallery.length > 0
// ? product.images.gallery[0]
// : null) ||
// product.images?.primary ||
// (product.variants?.[0]?.images && product.variants[0].images.length > 0
// ? product.variants[0].images[0]
// : null) ||
// 'https://via.placeholder.com/300';
// productMap[product._id.toString()] = {
// ...product,
// displayImage: image,
// };
// });
// const enhancedRecentOrders = recentOrders.map(order => ({
// ...order,
// items: order.items.map(item => ({
// ...item,
// productImage: productMap[item.productId]?.displayImage || 'https://via.placeholder.com/300',
// productDetails: productMap[item.productId] || null,
// })),
// }));
// res.status(200).json({
// statusCode: 200,
// status: true,
// message: 'Dashboard stats fetched successfully',
// data: {
// // Summary Stats
// totalUsers,
// totalOrders,
// totalProducts,
// totalRevenue: parseFloat(totalRevenue._sum.totalAmount || 0),
// // Lists
// recentOrders: enhancedRecentOrders,
// topProducts,
// // ✅ Graph Data
// charts: {
// orderOverview,
// revenueOverview,
// ordersByStatus,
// monthlyComparison,
// },
// },
// });
// } catch (error) {
// console.error('Dashboard stats error:', error);
// next(error);
// }
// };
// ==========================================
// HELPER FUNCTIONS FOR GRAPHS
// ==========================================
exports.getDashboardStats = async (req, res, next) => {
try {
const [
totalUsers,
totalOrders,
totalProducts,
totalRevenue,
recentOrders,
topSellingProducts,
lowStockProducts,
inventoryStats,
orderOverview,
revenueOverview,
ordersByStatus,
monthlyComparison,
] = await Promise.all([
prisma.user.count(),
prisma.order.count(),
Product.countDocuments({ status: 'active' }),
prisma.order.aggregate({
_sum: { totalAmount: true },
where: { paymentStatus: 'PAID' },
}),
prisma.order.findMany({
take: 5,
orderBy: { createdAt: 'desc' },
include: {
user: {
select: { id: true, email: true, firstName: true, lastName: true },
},
items: true,
},
}),
getTopSellingProducts(), // ✅ Real top sellers
getLowStockProducts(10), // ✅ Low stock alerts
getInventoryStats(), // ✅ Inventory overview
getOrderOverview(),
getRevenueOverview(),
getOrdersByStatus(),
getMonthlyComparison(),
]);
// Enhance recent orders with product images
const orderProductIds = recentOrders
.flatMap(order => order.items.map(item => item.productId))
.filter(Boolean);
const orderProducts = await Product.find({
_id: { $in: orderProductIds }
}).lean();
const productMap = {};
orderProducts.forEach(product => {
productMap[product._id.toString()] = {
...product,
displayImage: getProductImage(product),
};
});
const enhancedRecentOrders = recentOrders.map(order => ({
...order,
items: order.items.map(item => ({
...item,
productImage: productMap[item.productId]?.displayImage || 'https://via.placeholder.com/300',
productDetails: productMap[item.productId] || null,
})),
}));
res.status(200).json({
statusCode: 200,
status: true,
message: 'Dashboard stats fetched successfully',
data: {
// Summary Stats
totalUsers,
totalOrders,
totalProducts,
totalRevenue: parseFloat(totalRevenue._sum.totalAmount || 0),
// ✅ Inventory Stats
inventory: inventoryStats,
// Lists
recentOrders: enhancedRecentOrders,
topProducts: topSellingProducts, // ✅ Real selling data
lowStockProducts, // ✅ Products needing restock
// Charts
charts: {
orderOverview,
revenueOverview,
ordersByStatus,
monthlyComparison,
},
},
});
} catch (error) {
console.error('Dashboard stats error:', error);
next(error);
}
};
// ✅ GET REAL TOP SELLING PRODUCTS
async function getTopSellingProducts() {
try {
// Get sales data from order items
const salesData = await prisma.orderItem.groupBy({
by: ['productId'],
_sum: { quantity: true },
_count: { productId: true },
orderBy: { _sum: { quantity: 'desc' } },
take: 10,
});
if (salesData.length === 0) {
// Fallback: Return recent products
const fallback = await Product.find({ status: 'active' })
.sort({ createdAt: -1 })
.limit(5)
.lean();
return fallback.map(p => ({
...p,
_id: p._id.toString(),
displayImage: getProductImage(p),
totalSold: 0,
totalOrders: 0,
stock: p.stock || 0,
stockStatus: getStockStatus(p.stock || 0),
}));
}
const productIds = salesData.map(item => item.productId);
const products = await Product.find({
_id: { $in: productIds },
status: 'active',
}).lean();
const statsMap = {};
salesData.forEach(item => {
statsMap[item.productId] = {
totalSold: item._sum.quantity || 0,
totalOrders: item._count.productId || 0,
};
});
const topProducts = products
.map(product => {
const stats = statsMap[product._id.toString()] || { totalSold: 0, totalOrders: 0 };
const stock = product.stock || 0;
return {
_id: product._id.toString(),
name: product.name,
slug: product.slug,
basePrice: product.basePrice,
displayImage: getProductImage(product),
totalSold: stats.totalSold,
totalOrders: stats.totalOrders,
revenue: stats.totalSold * (product.basePrice || 0),
stock: stock, // ✅ Current stock
stockStatus: getStockStatus(stock), // ✅ Stock status
};
})
.sort((a, b) => b.totalSold - a.totalSold)
.slice(0, 5);
return topProducts;
} catch (error) {
console.error('Error fetching top selling products:', error);
return [];
}
}
function getStockStatus(stock) {
if (stock === 0) return 'OUT_OF_STOCK';
if (stock <= 5) return 'CRITICAL';
if (stock <= 10) return 'LOW';
return 'IN_STOCK';
}
function getProductImage(product) {
return (
(product.images?.gallery?.[0]) ||
product.images?.primary ||
(product.variants?.[0]?.images?.[0]) ||
'https://via.placeholder.com/300'
);
}
/**
* Get daily order count for last 30 days
*/
// async function getOrderOverview() {
// const thirtyDaysAgo = new Date();
// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// // Group orders by date
// const orders = await prisma.order.findMany({
// where: {
// createdAt: {
// gte: thirtyDaysAgo,
// },
// },
// select: {
// createdAt: true,
// status: true,
// },
// });
// // Create date map for last 30 days
// const dateMap = {};
// for (let i = 29; i >= 0; i--) {
// const date = new Date();
// date.setDate(date.getDate() - i);
// const dateKey = date.toISOString().split('T')[0];
// dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 };
// }
// // Count orders by date
// orders.forEach(order => {
// const dateKey = order.createdAt.toISOString().split('T')[0];
// if (dateMap[dateKey]) {
// dateMap[dateKey].total++;
// if (order.status === 'DELIVERED') {
// dateMap[dateKey].completed++;
// } else if (['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)) {
// dateMap[dateKey].pending++;
// } else if (order.status === 'CANCELLED') {
// dateMap[dateKey].cancelled++;
// }
// }
// });
// // Convert to array format for charts
// return Object.entries(dateMap).map(([date, counts]) => ({
// date,
// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
// total: counts.total,
// completed: counts.completed,
// pending: counts.pending,
// cancelled: counts.cancelled,
// }));
// }
async function getOrderOverview() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const orders = await prisma.order.findMany({
where: { createdAt: { gte: thirtyDaysAgo } },
select: { createdAt: true, status: true },
});
const dateMap = {};
for (let i = 29; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateKey = date.toISOString().split('T')[0];
dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 };
}
orders.forEach(order => {
const dateKey = order.createdAt.toISOString().split('T')[0];
if (dateMap[dateKey]) {
dateMap[dateKey].total++;
if (order.status === 'DELIVERED') dateMap[dateKey].completed++;
else if (['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)) dateMap[dateKey].pending++;
else if (order.status === 'CANCELLED') dateMap[dateKey].cancelled++;
}
});
return Object.entries(dateMap).map(([date, counts]) => ({
date,
label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
...counts,
}));
}
/**
* Get daily revenue for last 30 days
*/
// async function getRevenueOverview() {
// const thirtyDaysAgo = new Date();
// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// const orders = await prisma.order.findMany({
// where: {
// createdAt: {
// gte: thirtyDaysAgo,
// },
// paymentStatus: 'PAID',
// },
// select: {
// createdAt: true,
// totalAmount: true,
// },
// });
// // Create date map
// const dateMap = {};
// for (let i = 29; i >= 0; i--) {
// const date = new Date();
// date.setDate(date.getDate() - i);
// const dateKey = date.toISOString().split('T')[0];
// dateMap[dateKey] = 0;
// }
// // Sum revenue by date
// orders.forEach(order => {
// const dateKey = order.createdAt.toISOString().split('T')[0];
// if (dateMap[dateKey] !== undefined) {
// dateMap[dateKey] += parseFloat(order.totalAmount);
// }
// });
// return Object.entries(dateMap).map(([date, revenue]) => ({
// date,
// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
// revenue: Math.round(revenue * 100) / 100, // Round to 2 decimals
// }));
// }
async function getRevenueOverview() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const orders = await prisma.order.findMany({
where: { createdAt: { gte: thirtyDaysAgo }, paymentStatus: 'PAID' },
select: { createdAt: true, totalAmount: true },
});
const dateMap = {};
for (let i = 29; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
dateMap[date.toISOString().split('T')[0]] = 0;
}
orders.forEach(order => {
const dateKey = order.createdAt.toISOString().split('T')[0];
if (dateMap[dateKey] !== undefined) {
dateMap[dateKey] += parseFloat(order.totalAmount);
}
});
return Object.entries(dateMap).map(([date, revenue]) => ({
date,
label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
revenue: Math.round(revenue * 100) / 100,
}));
}
/**
* Get order counts by status
*/
// async function getOrdersByStatus() {
// const statusCounts = await prisma.order.groupBy({
// by: ['status'],
// _count: true,
// });
// const statusLabels = {
// PENDING: 'Pending',
// CONFIRMED: 'Confirmed',
// PROCESSING: 'Processing',
// SHIPPED: 'Shipped',
// DELIVERED: 'Delivered',
// CANCELLED: 'Cancelled',
// RETURN_REQUESTED: 'Return Requested',
// };
// const statusColors = {
// PENDING: '#FCD34D',
// CONFIRMED: '#60A5FA',
// PROCESSING: '#A78BFA',
// SHIPPED: '#C084FC',
// DELIVERED: '#34D399',
// CANCELLED: '#F87171',
// RETURN_REQUESTED: '#FB923C',
// };
// return statusCounts.map(item => ({
// status: item.status,
// label: statusLabels[item.status] || item.status,
// count: item._count,
// color: statusColors[item.status] || '#9CA3AF',
// }));
// }
async function getOrdersByStatus() {
const statusCounts = await prisma.order.groupBy({
by: ['status'],
_count: true,
});
const labels = {
PENDING: 'Pending',
CONFIRMED: 'Confirmed',
PROCESSING: 'Processing',
SHIPPED: 'Shipped',
DELIVERED: 'Delivered',
CANCELLED: 'Cancelled',
RETURN_REQUESTED: 'Return Requested',
};
const colors = {
PENDING: '#FCD34D',
CONFIRMED: '#60A5FA',
PROCESSING: '#A78BFA',
SHIPPED: '#C084FC',
DELIVERED: '#34D399',
CANCELLED: '#F87171',
RETURN_REQUESTED: '#FB923C',
};
return statusCounts.map(item => ({
status: item.status,
label: labels[item.status] || item.status,
count: item._count,
color: colors[item.status] || '#9CA3AF',
}));
}
/**
* Compare current month vs previous month
*/
// async function getMonthlyComparison() {
// const now = new Date();
// // Current month dates
// const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
// const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
// // Previous month dates
// const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
// const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
// const [currentMonth, previousMonth] = await Promise.all([
// prisma.order.aggregate({
// where: {
// createdAt: {
// gte: currentMonthStart,
// lte: currentMonthEnd,
// },
// },
// _count: true,
// _sum: {
// totalAmount: true,
// },
// }),
// prisma.order.aggregate({
// where: {
// createdAt: {
// gte: previousMonthStart,
// lte: previousMonthEnd,
// },
// },
// _count: true,
// _sum: {
// totalAmount: true,
// },
// }),
// ]);
// const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0);
// const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0);
// const orderGrowth = previousMonth._count > 0
// ? ((currentMonth._count - previousMonth._count) / previousMonth._count) * 100
// : 100;
// const revenueGrowth = previousRevenue > 0
// ? ((currentRevenue - previousRevenue) / previousRevenue) * 100
// : 100;
// return {
// currentMonth: {
// orders: currentMonth._count,
// revenue: Math.round(currentRevenue * 100) / 100,
// label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
// },
// previousMonth: {
// orders: previousMonth._count,
// revenue: Math.round(previousRevenue * 100) / 100,
// label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
// },
// growth: {
// orders: Math.round(orderGrowth * 10) / 10,
// revenue: Math.round(revenueGrowth * 10) / 10,
// },
// };
// }
async function getMonthlyComparison() {
const now = new Date();
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
const [currentMonth, previousMonth] = await Promise.all([
prisma.order.aggregate({
where: { createdAt: { gte: currentMonthStart, lte: currentMonthEnd } },
_count: true,
_sum: { totalAmount: true },
}),
prisma.order.aggregate({
where: { createdAt: { gte: previousMonthStart, lte: previousMonthEnd } },
_count: true,
_sum: { totalAmount: true },
}),
]);
const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0);
const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0);
return {
currentMonth: {
orders: currentMonth._count,
revenue: Math.round(currentRevenue * 100) / 100,
label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
},
previousMonth: {
orders: previousMonth._count,
revenue: Math.round(previousRevenue * 100) / 100,
label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
},
growth: {
orders: previousMonth._count > 0 ? Math.round(((currentMonth._count - previousMonth._count) / previousMonth._count) * 1000) / 10 : 100,
revenue: previousRevenue > 0 ? Math.round(((currentRevenue - previousRevenue) / previousRevenue) * 1000) / 10 : 100,
},
};
}

View File

@@ -0,0 +1,395 @@
const { prisma } = require('../../config/database');
// exports.getAllOrders = async (req, res, next) => {
// try {
// const { page = 1, limit = 20, status, paymentStatus, userId } = req.query;
// const skip = (page - 1) * limit;
// const where = {};
// if (status) where.status = status;
// if (paymentStatus) where.paymentStatus = paymentStatus;
// if (userId) where.userId = userId;
// 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: +skip,
// take: +limit,
// }),
// prisma.order.count({ where }),
// ]);
// // res.json({
// // success: true,
// // data: { orders, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
// // });
// res.status(200).json({
// statusCode: 200,
// status: true,
// message: 'Orders fetched successfully',
// data: {
// orders,
// pagination: {
// page: +page,
// limit: +limit,
// total,
// pages: Math.ceil(total / limit),
// },
// },
// });
// } catch (error) {
// next(error);
// }
// };
// exports.getOrderDetails = async (req, res, next) => {
// try {
// const { id } = req.params;
// const order = await prisma.order.findUnique({
// where: { id },
// include: {
// items: {
// include: {
// product: true, // if you want product details
// }
// },
// address: true,
// user: {
// select: {
// id: true,
// firstName: true,
// lastName: true,
// email: true,
// phone: true,
// },
// },
// },
// });
// if (!order) {
// return res.status(404).json({
// success: false,
// message: "Order not found",
// });
// }
// res.json({
// success: true,
// data: order,
// });
// } catch (error) {
// next(error);
// }
// };
exports.getAllOrders = async (req, res, next) => {
try {
const {
page = 1,
limit = 20,
status,
search,
sortBy = 'createdAt',
sortOrder = 'desc',
} = req.query;
const skip = (parseInt(page) - 1) * parseInt(limit);
const where = {};
if (status) {
where.status = status;
}
if (search) {
where.OR = [
{ orderNumber: { contains: search, mode: 'insensitive' } },
{ trackingNumber: { contains: search, mode: 'insensitive' } },
];
}
const [orders, total] = await Promise.all([
prisma.order.findMany({
where,
include: {
user: {
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
},
},
address: true,
items: true,
// ✅ Include latest status change
statusHistory: {
take: 1,
orderBy: { createdAt: 'desc' },
include: {
admin: {
select: {
firstName: true,
lastName: true,
},
},
},
},
},
orderBy: {
[sortBy]: sortOrder,
},
skip,
take: parseInt(limit),
}),
prisma.order.count({ where }),
]);
res.status(200).json({
success: true,
data: {
orders,
pagination: {
total,
page: parseInt(page),
limit: parseInt(limit),
pages: Math.ceil(total / parseInt(limit)),
},
},
});
} catch (error) {
console.error('❌ Get all orders error:', error);
next(error);
}
};
// **
// * @desc Get single order with full history
// * @route GET /api/admin/orders/:orderId
// * @access Private/Admin
// */
exports.getOrderById = async (req, res, next) => {
try {
const { orderId } = req.params;
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: {
select: {
id: true,
email: true,
firstName: true,
lastName: true,
phone: true,
},
},
address: true,
items: true,
// ✅ Include full status history
statusHistory: {
orderBy: { createdAt: 'desc' },
include: {
admin: {
select: {
id: true,
email: true,
firstName: true,
lastName: true,
},
},
},
},
},
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found',
});
}
res.status(200).json({
success: true,
data: order,
});
} catch (error) {
console.error('❌ Get order 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 Get status change statistics
* @route GET /api/admin/orders/stats/status-changes
* @access Private/Admin
*/
exports.getStatusChangeStats = async (req, res, next) => {
try {
const { startDate, endDate } = req.query;
const where = {};
if (startDate || endDate) {
where.createdAt = {};
if (startDate) where.createdAt.gte = new Date(startDate);
if (endDate) where.createdAt.lte = new Date(endDate);
}
// Count status changes by status
const statusChanges = await prisma.orderStatusHistory.groupBy({
by: ['toStatus'],
where,
_count: true,
});
// Count status changes by admin
const changesByAdmin = await prisma.orderStatusHistory.groupBy({
by: ['changedBy'],
where,
_count: true,
orderBy: {
_count: {
changedBy: 'desc',
},
},
take: 10,
});
// Get admin details
const adminIds = changesByAdmin
.map(item => item.changedBy)
.filter(Boolean);
const admins = await prisma.user.findMany({
where: { id: { in: adminIds } },
select: {
id: true,
firstName: true,
lastName: true,
email: true,
},
});
const adminMap = Object.fromEntries(
admins.map(admin => [admin.id, admin])
);
const changesByAdminWithDetails = changesByAdmin.map(item => ({
admin: item.changedBy ? adminMap[item.changedBy] : null,
count: item._count,
}));
res.status(200).json({
success: true,
data: {
statusChanges,
changesByAdmin: changesByAdminWithDetails,
},
});
} catch (error) {
console.error('❌ Get status stats error:', error);
next(error);
}
};
exports.getOrderDetails = async (req, res, next) => {
try {
const { id } = req.params;
const order = await prisma.order.findUnique({
where: { id },
include: {
items: true, // OrderItem snapshot data
address: true, // shipping address
user: {
select: {
id: true,
firstName: true,
lastName: true,
email: true,
phone: true,
},
},
},
});
if (!order) {
return res.status(404).json({
// success: false,
// message: "Order not found",
statusCode: 404,
status: false,
message: 'Order not found',
});
}
// res.json({
// success: true,
// data: order,
// });
res.status(200).json({
statusCode: 200,
status: true,
message: 'Order details fetched successfully',
data: order,
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,692 @@
const Product = require('../../models/mongodb/Product');
const uploadToS3 = require("../../utils/uploadToS3");
exports.getAllProducts = async (req, res, next) => {
try {
const { page = 1, limit = 20, status, category, search } = req.query;
const skip = (page - 1) * limit;
const query = {};
if (status) query.status = status;
if (category) query.category = category;
if (search) query.$text = { $search: search };
const [products, total] = await Promise.all([
Product.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(+limit)
.lean(),
Product.countDocuments(query),
]);
// res.json({
// success: true,
// data: { products, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Products fetched successfully',
data: {
products,
pagination: {
page: +page,
limit: +limit,
total,
pages: Math.ceil(total / limit),
},
},
});
} catch (error) {
next(error);
}
};
// exports.createProduct = async (req, res, next) => {
// try {
// const product = new Product(req.body);
// await product.save();
// res.status(201).json({ success: true, message: 'Product created', data: product });
// } catch (error) {
// next(error);
// }
// };
// exports.createProduct = async (req, res, next) => {
// try {
// let { name, slug, ...rest } = req.body;
// // Generate slug from name if not provided
// if (!slug) {
// slug = name
// .toLowerCase()
// .replace(/[^a-z0-9]+/g, '-') // replace spaces & special chars
// .replace(/(^-|-$)/g, ''); // remove leading/trailing hyphens
// }
// // Ensure slug is unique
// let slugExists = await Product.findOne({ slug });
// let counter = 1;
// const baseSlug = slug;
// while (slugExists) {
// slug = `${baseSlug}-${counter}`;
// slugExists = await Product.findOne({ slug });
// counter++;
// }
// // ✅ Upload images manually
// const primaryImage = req.files?.primaryImage
// ? await uploadToS3(req.files.primaryImage[0])
// : null;
// const galleryImages = req.files?.galleryImages
// ? await Promise.all(
// req.files.galleryImages.map((file) => uploadToS3(file))
// )
// : [];
// // const product = new Product({ name, slug, ...rest });
// const product = new Product({
// name,
// slug,
// ...rest,
// images: {
// primary: primaryImage,
// gallery: galleryImages,
// },
// });
// await product.save();
// // res.status(201).json({ success: true, message: 'Product created', data: product });
// return res.status(201).json({
// // statusCode: 201,
// status: true,
// message: 'Product created successfully',
// data: product,
// });
// } catch (error) {
// next(error);
// }
// };
// exports.createProduct = async (req, res, next) => {
// try {
// let { name, hasVariants, variants, ...rest } = req.body;
// let slug;
// // slug generation
// if (!slug) {
// slug = name
// .toLowerCase()
// .replace(/[^a-z0-9]+/g, "-")
// .replace(/(^-|-$)/g, "");
// }
// let exists = await Product.findOne({ slug });
// let i = 1;
// while (exists) {
// slug = `${slug}-${i++}`;
// exists = await Product.findOne({ slug });
// }
// // Upload main images
// const primaryImage = req.files?.primaryImage
// ? await uploadToS3(req.files.primaryImage[0])
// : null;
// const galleryImages = req.files?.galleryImages
// ? await Promise.all(
// req.files.galleryImages.map((f) => uploadToS3(f))
// )
// : [];
// const productData = {
// name,
// slug,
// ...rest,
// hasVariants: hasVariants === "true",
// };
// // 🔥 VARIANT LOGIC
// if (hasVariants === "true") {
// const parsedVariants = JSON.parse(variants); // IMPORTANT
// productData.variants = await Promise.all(
// parsedVariants.map(async (variant) => {
// const variantImages =
// req.files?.[`variantImages_${variant.color}`]
// ? await Promise.all(
// req.files[`variantImages_${variant.color}`].map(uploadToS3)
// )
// : [];
// return {
// size: variant.size,
// color: variant.color,
// sku: variant.sku,
// price: Number(variant.price),
// compareAtPrice: Number(variant.compareAtPrice),
// inventory: {
// quantity: Number(variant.quantity),
// trackInventory: true,
// },
// images: variantImages,
// };
// })
// );
// } else {
// // simple product images
// productData.images = {
// primary: primaryImage,
// gallery: galleryImages,
// };
// }
// const product = await Product.create(productData);
// res.status(201).json({
// status: true,
// message: "Product created successfully",
// data: product,
// });
// } catch (err) {
// next(err);
// }
// };
// exports.createProduct = async (req, res, next) => {
// try {
// const { name, hasVariants, variants, ...rest } = req.body;
// // ✅ Validate name
// if (!name || name.trim() === "") {
// return res.status(400).json({
// status: false,
// message: "Product name is required",
// });
// }
// // slug generation
// let slug = name
// .toLowerCase()
// .replace(/[^a-z0-9]+/g, "-")
// .replace(/(^-|-$)/g, "");
// // ensure unique slug
// let exists = await Product.findOne({ slug });
// let i = 1;
// while (exists) {
// slug = `${slug}-${i++}`;
// exists = await Product.findOne({ slug });
// }
// // Upload main images
// const primaryImage = req.files?.primaryImage
// ? await uploadToS3(req.files.primaryImage[0])
// : null;
// const galleryImages = req.files?.galleryImages
// ? await Promise.all(req.files.galleryImages.map(uploadToS3))
// : [];
// const productData = {
// name,
// slug,
// ...rest,
// hasVariants: hasVariants === "true",
// };
// // 🔥 VARIANT LOGIC
// if (hasVariants === "true") {
// const parsedVariants = JSON.parse(variants); // IMPORTANT
// productData.variants = await Promise.all(
// parsedVariants.map(async (variant) => {
// const variantImages =
// req.files?.[`variantImages_${variant.color}`]
// ? await Promise.all(
// req.files[`variantImages_${variant.color}`].map(uploadToS3)
// )
// : [];
// return {
// size: variant.size,
// color: variant.color,
// sku: variant.sku,
// price: Number(variant.price),
// compareAtPrice: Number(variant.compareAtPrice),
// inventory: {
// quantity: Number(variant.quantity),
// trackInventory: true,
// },
// images: variantImages,
// };
// })
// );
// } else {
// // simple product images
// productData.images = {
// primary: primaryImage,
// gallery: galleryImages,
// };
// }
// const product = await Product.create(productData);
// res.status(201).json({
// status: true,
// message: "Product created successfully",
// data: product,
// });
// } catch (err) {
// next(err);
// }
// };
// exports.createProduct = async (req, res, next) => {
// try {
// const { name, hasVariants, variants, ...rest } = req.body;
// console.log('📥 Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' });
// console.log('📥 Files received:', req.files ? req.files.length : 0);
// // ✅ Validate name
// if (!name || name.trim() === "") {
// return res.status(400).json({
// status: false,
// message: "Product name is required",
// });
// }
// // Generate unique slug
// let slug = name
// .toLowerCase()
// .replace(/[^a-z0-9]+/g, "-")
// .replace(/(^-|-$)/g, "");
// let exists = await Product.findOne({ slug });
// let i = 1;
// while (exists) {
// slug = `${slug}-${i++}`;
// exists = await Product.findOne({ slug });
// }
// const productData = {
// name,
// slug,
// ...rest,
// hasVariants: hasVariants === "true" || hasVariants === true,
// };
// // ======================
// // VARIANT MODE
// // ======================
// if (productData.hasVariants) {
// console.log('🎨 Processing variant product...');
// if (!variants) {
// return res.status(400).json({
// status: false,
// message: "Variants data is required when hasVariants is true",
// });
// }
// const parsedVariants = JSON.parse(variants);
// console.log('📦 Parsed variants:', parsedVariants.length);
// // ✅ Convert req.files array to object grouped by fieldname
// const filesGrouped = {};
// if (req.files && req.files.length > 0) {
// req.files.forEach(file => {
// if (!filesGrouped[file.fieldname]) {
// filesGrouped[file.fieldname] = [];
// }
// filesGrouped[file.fieldname].push(file);
// });
// }
// console.log('📸 Files grouped:', Object.keys(filesGrouped));
// // Process each variant
// productData.variants = await Promise.all(
// parsedVariants.map(async (variant) => {
// const color = variant.color;
// const fieldName = `variantImages_${color}`;
// console.log(`🔍 Looking for images with fieldname: ${fieldName}`);
// // Get images for this variant
// const variantFiles = filesGrouped[fieldName] || [];
// console.log(`📸 Found ${variantFiles.length} images for ${color}`);
// // Upload images to S3
// const variantImages = variantFiles.length > 0
// ? await Promise.all(variantFiles.map(uploadToS3))
// : [];
// console.log(`✅ Uploaded ${variantImages.length} images for ${color}`);
// return {
// size: variant.size || 'default',
// color: variant.color,
// sku: variant.sku,
// price: Number(variant.price),
// compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null,
// inventory: {
// quantity: Number(variant.quantity || variant.stock || 0),
// trackInventory: true,
// },
// images: variantImages,
// isActive: true,
// };
// })
// );
// console.log('✅ All variants processed:', productData.variants.length);
// }
// // ======================
// // SIMPLE PRODUCT MODE
// // ======================
// else {
// console.log('📦 Processing simple product...');
// // ✅ Handle files from req.files array
// let primaryImage = null;
// let galleryImages = [];
// if (req.files && req.files.length > 0) {
// // Group files by fieldname
// const filesGrouped = {};
// req.files.forEach(file => {
// if (!filesGrouped[file.fieldname]) {
// filesGrouped[file.fieldname] = [];
// }
// filesGrouped[file.fieldname].push(file);
// });
// // Upload primary image
// if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) {
// primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]);
// }
// // Upload gallery images
// if (filesGrouped['galleryImages']) {
// galleryImages = await Promise.all(
// filesGrouped['galleryImages'].map(uploadToS3)
// );
// }
// }
// productData.images = {
// primary: primaryImage,
// gallery: galleryImages,
// videos: [],
// };
// console.log('✅ Images uploaded:', {
// primary: !!primaryImage,
// gallery: galleryImages.length,
// });
// }
// // Create product in MongoDB
// const product = await Product.create(productData);
// console.log('✅ Product created:', product._id);
// res.status(201).json({
// status: true,
// message: "Product created successfully",
// data: product,
// });
// } catch (err) {
// console.error('❌ Error creating product:', err);
// // Send detailed error for debugging
// if (process.env.NODE_ENV === 'development') {
// return res.status(400).json({
// status: false,
// message: "Failed to create product",
// error: err.message,
// stack: err.stack,
// });
// }
// next(err);
// }
// };
exports.createProduct = async (req, res, next) => {
try {
const { name, hasVariants, variants, ...rest } = req.body;
console.log('📥 Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' });
console.log('📥 Files received:', req.files ? req.files.length : 0);
// ✅ Validate name
if (!name || name.trim() === "") {
return res.status(400).json({
status: false,
message: "Product name is required",
});
}
// Generate unique slug
let slug = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
let exists = await Product.findOne({ slug });
let i = 1;
while (exists) {
slug = `${slug}-${i++}`;
exists = await Product.findOne({ slug });
}
// ✅ Build productData carefully to avoid empty slug
const productData = {
name,
slug, // Generated slug
hasVariants: hasVariants === "true" || hasVariants === true,
};
// ✅ Add other fields from rest, but skip 'slug' if it exists
Object.keys(rest).forEach(key => {
if (key !== 'slug') {
productData[key] = rest[key];
}
});
// ======================
// VARIANT MODE
// ======================
if (productData.hasVariants) {
console.log('🎨 Processing variant product...');
if (!variants) {
return res.status(400).json({
status: false,
message: "Variants data is required when hasVariants is true",
});
}
const parsedVariants = JSON.parse(variants);
console.log('📦 Parsed variants:', parsedVariants.length);
// ✅ Convert req.files array to object grouped by fieldname
const filesGrouped = {};
if (req.files && req.files.length > 0) {
req.files.forEach(file => {
if (!filesGrouped[file.fieldname]) {
filesGrouped[file.fieldname] = [];
}
filesGrouped[file.fieldname].push(file);
});
}
console.log('📸 Files grouped:', Object.keys(filesGrouped));
// Process each variant
productData.variants = await Promise.all(
parsedVariants.map(async (variant) => {
const color = variant.color;
const fieldName = `variantImages_${color}`;
console.log(`🔍 Looking for images with fieldname: ${fieldName}`);
// Get images for this variant
const variantFiles = filesGrouped[fieldName] || [];
console.log(`📸 Found ${variantFiles.length} images for ${color}`);
// Upload images to S3
const variantImages = variantFiles.length > 0
? await Promise.all(variantFiles.map(uploadToS3))
: [];
console.log(`✅ Uploaded ${variantImages.length} images for ${color}`);
return {
size: variant.size || 'default',
color: variant.color,
sku: variant.sku,
price: Number(variant.price),
compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null,
inventory: {
quantity: Number(variant.quantity || variant.stock || 0),
trackInventory: true,
},
images: variantImages,
isActive: true,
};
})
);
console.log('✅ All variants processed:', productData.variants.length);
}
// ======================
// SIMPLE PRODUCT MODE
// ======================
else {
console.log('📦 Processing simple product...');
// ✅ Handle files from req.files array
let primaryImage = null;
let galleryImages = [];
if (req.files && req.files.length > 0) {
// Group files by fieldname
const filesGrouped = {};
req.files.forEach(file => {
if (!filesGrouped[file.fieldname]) {
filesGrouped[file.fieldname] = [];
}
filesGrouped[file.fieldname].push(file);
});
// Upload primary image
if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) {
primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]);
}
// Upload gallery images
if (filesGrouped['galleryImages']) {
galleryImages = await Promise.all(
filesGrouped['galleryImages'].map(uploadToS3)
);
}
}
productData.images = {
primary: primaryImage,
gallery: galleryImages,
videos: [],
};
console.log('✅ Images uploaded:', {
primary: !!primaryImage,
gallery: galleryImages.length,
});
}
// Create product in MongoDB
const product = await Product.create(productData);
console.log('✅ Product created:', product._id);
res.status(201).json({
status: true,
message: "Product created successfully",
data: product,
});
} catch (err) {
console.error('❌ Error creating product:', err);
// Send detailed error for debugging
if (process.env.NODE_ENV === 'development') {
return res.status(400).json({
status: false,
message: "Failed to create product",
error: err.message,
stack: err.stack,
});
}
next(err);
}
};
exports.updateProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
new: true,
});
// if (!product) return res.status(404).json({ success: false, message: 'Product not found' });
if (!product) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Product not found',
});
}
// res.json({ success: true, message: 'Product updated', data: product });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Product updated successfully',
data: product,
});
} catch (error) {
next(error);
}
};
exports.deleteProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
// if (!product) return res.status(404).json({ success: false, message: 'Product not found' });
if (!product) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Product not found',
});
}
// res.json({ success: true, message: 'Product deleted' });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Product deleted successfully',
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,411 @@
const { prisma } = require('../../config/database');
const Product = require('../../models/mongodb/Product');
// const ReturnModel = require("../../models/mongodb/Return");
const Order = prisma.order;
module.exports = {
// ===============================
// 📌 1. OVERVIEW KPI
// ===============================
getOverviewReport: async (req, res, next) => {
try {
const [
totalUsers,
totalOrders,
totalProducts,
totalRevenue,
totalCustomers,
totalSellers,
] = await Promise.all([
prisma.user.count(),
prisma.order.count(),
Product.countDocuments(),
prisma.order.aggregate({
_sum: { totalAmount: true },
where: { paymentStatus: 'PAID' },
}),
prisma.user.count({ where: { role: 'CUSTOMER' } }),
prisma.user.count({ where: { role: 'SELLER' } }),
]);
res.json({
success: true,
data: {
totalUsers,
totalCustomers,
totalSellers,
totalOrders,
totalProducts,
totalRevenue: totalRevenue._sum.totalAmount || 0,
},
});
} catch (error) {
next(error);
}
},
// ===============================
// 📌 2. SALES ANALYTICS (Daily + Monthly)
// ===============================
getSalesAnalytics: async (req, res, next) => {
try {
const today = new Date();
const last30Days = new Date();
last30Days.setDate(today.getDate() - 30);
// 1⃣ Daily sales (last 30 days)
const dailyOrders = await prisma.order.findMany({
where: {
paymentStatus: 'PAID',
createdAt: { gte: last30Days },
},
select: {
totalAmount: true,
createdAt: true,
},
});
const dailySales = dailyOrders.reduce((acc, order) => {
const date = order.createdAt.toISOString().split('T')[0]; // YYYY-MM-DD
acc[date] = (acc[date] || 0) + Number(order.totalAmount);
return acc;
}, {});
// 2⃣ Monthly sales (all time)
const allPaidOrders = await prisma.order.findMany({
where: { paymentStatus: 'PAID' },
select: { totalAmount: true, createdAt: true },
});
const monthlySales = allPaidOrders.reduce((acc, order) => {
const month = order.createdAt.getMonth() + 1; // 1-12
const year = order.createdAt.getFullYear();
const key = `${year}-${month.toString().padStart(2, '0')}`; // YYYY-MM
acc[key] = (acc[key] || 0) + Number(order.totalAmount);
return acc;
}, {});
res.json({
success: true,
data: {
dailySales,
monthlySales,
},
});
} catch (error) {
next(error);
}
},
// ===============================
// 📌 3. CUSTOMER STATISTICS
// ===============================
// ===============================
// 📌 3. CUSTOMER STATISTICS (UPDATED)
// ===============================
getCustomerStats: async (req, res, next) => {
try {
const now = new Date();
// ---------- WEEKLY DATA (MonSun) ----------
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - now.getDay() + 1); // Monday
const weeklyData = await Promise.all(
Array.from({ length: 7 }).map(async (_, i) => {
const dayStart = new Date(startOfWeek);
dayStart.setDate(startOfWeek.getDate() + i);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayStart.getDate() + 1);
const count = await prisma.user.count({
where: {
role: 'CUSTOMER',
createdAt: {
gte: dayStart,
lt: dayEnd,
},
},
});
const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
return { label: dayLabels[i], value: count };
})
);
// ---------- MONTHLY DATA (JanDec) ----------
const monthlyData = await Promise.all(
Array.from({ length: 12 }).map(async (_, i) => {
const monthStart = new Date(now.getFullYear(), i, 1);
const monthEnd = new Date(now.getFullYear(), i + 1, 1);
const count = await prisma.user.count({
where: {
role: 'CUSTOMER',
createdAt: {
gte: monthStart,
lt: monthEnd,
},
},
});
const monthLabels = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return { label: monthLabels[i], value: count };
})
);
// ---------- YEARLY DATA (Last 5 Years) ----------
const currentYear = now.getFullYear();
const yearlyData = await Promise.all(
Array.from({ length: 5 }).map(async (_, i) => {
const year = currentYear - (4 - i);
const yearStart = new Date(year, 0, 1);
const yearEnd = new Date(year + 1, 0, 1);
const count = await prisma.user.count({
where: {
role: 'CUSTOMER',
createdAt: {
gte: yearStart,
lt: yearEnd,
},
},
});
return { label: year.toString(), value: count };
})
);
// ---------- CURRENT STATS ----------
const [newCustomers, repeatCustomers] = await Promise.all([
prisma.user.count({
where: {
role: 'CUSTOMER',
createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
},
}),
prisma.order.groupBy({
by: ['userId'],
_count: { id: true },
having: { id: { _count: { gt: 1 } } },
}),
]);
// ---------- RESPONSE ----------
res.json({
success: true,
data: {
newCustomers,
repeatCustomers: repeatCustomers.length,
graph: {
weekly: weeklyData,
monthly: monthlyData,
yearly: yearlyData,
},
},
});
} catch (error) {
console.error(error);
next(error);
}
},
// ===============================
// 📌 4. SELLER STATISTICS
// ===============================
getSellerStats: async (req, res, next) => {
try {
// Fetch all sellers from PostgreSQL
const sellers = await prisma.user.findMany({
where: { role: 'SELLER' },
});
// For each seller, count products from MongoDB
const formatted = await Promise.all(
sellers.map(async s => {
const totalProducts = await Product.countDocuments({
sellerId: s.id,
});
return {
sellerId: s.id,
name: `${s.firstName} ${s.lastName}`,
totalProducts,
};
})
);
res.json({ success: true, data: formatted });
} catch (error) {
next(error);
}
},
// ===============================
// 📌 5. ORDER ANALYTICS
// ===============================
getOrderAnalytics: async (req, res, next) => {
try {
const orders = await prisma.order.groupBy({
by: ['status'],
_count: { id: true },
});
res.json({ success: true, data: orders });
} catch (error) {
next(error);
}
},
// ===============================
// 📌 6. RETURN / REFUND REPORT
// ===============================
// getReturnAnalytics: async (req, res, next) => {
// try {
// const totalReturns = await ReturnModel.countDocuments();
// const returnReasons = await ReturnModel.aggregate([
// {
// $group: {
// _id: "$reason",
// count: { $sum: 1 },
// },
// },
// ]);
// res.json({
// success: true,
// data: {
// totalReturns,
// returnReasons,
// },
// });
// } catch (error) {
// next(error);
// }
// },
// ===============================
// 📌 7. INVENTORY & STOCK REPORT
// ===============================
// Controller: getInventoryStats
getInventoryStats: async (req, res, next) => {
try {
const [lowStock, outOfStock, fastMoving] = await Promise.all([
Product.find({ stock: { $lte: 5, $gt: 0 } })
.select('_id name stock category')
.populate('category', 'name'), // get only category name
Product.find({ stock: 0 })
.select('_id name stock category')
.populate('category', 'name'),
Product.find()
.sort({ purchaseCount: -1 })
.limit(10)
.select('_id name stock category purchaseCount')
.populate('category', 'name'),
]);
res.json({
success: true,
data: {
lowStock,
outOfStock,
fastMoving,
},
});
} catch (error) {
next(error);
}
},
// ===============================
// 📌 Financial Stats (Safe Version)
// ===============================
getFinancialStats: async (req, res, next) => {
try {
// Total revenue from PAID orders
const totalRevenue = await prisma.order.aggregate({
_sum: { totalAmount: true },
where: { paymentStatus: 'PAID' },
});
// Total number of orders
const totalOrders = await prisma.order.count();
// Total number of paid orders
const paidOrders = await prisma.order.count({
where: { paymentStatus: 'PAID' },
});
res.json({
success: true,
data: {
totalRevenue: totalRevenue._sum.totalAmount || 0,
totalOrders,
paidOrders,
},
});
} catch (error) {
next(error);
}
},
// ===============================
// 📌 9. PAYOUT / TRANSFER HISTORY
// ===============================
getPayoutHistory: async (req, res, next) => {
try {
// Fetch all delivered orders with user info
const orders = await prisma.order.findMany({
where: { status: 'DELIVERED' },
include: { user: true },
orderBy: { deliveredAt: 'desc' },
});
const formatted = orders.map(order => ({
orderId: order.id,
orderNumber: order.orderNumber,
sellerId: order.userId,
sellerName: `${order.user.firstName} ${order.user.lastName}`,
totalAmount: Number(order.totalAmount),
deliveredAt: order.deliveredAt,
}));
res.json({ success: true, data: formatted });
} catch (error) {
next(error);
}
},
// ===============================
// 📌 10. REAL-TIME ACTIVITY FEED
// ===============================
getActivityFeed: async (req, res, next) => {
try {
const logs = await prisma.activityLog.findMany({
orderBy: { createdAt: 'desc' },
take: 20,
});
res.json({ success: true, data: logs });
} catch (error) {
next(error);
}
},
};

View File

@@ -0,0 +1,160 @@
const { prisma } = require('../../config/database');
exports.getAllUsers = async (req, res, next) => {
try {
const { page = 1, limit = 20, role, search, isActive } = req.query;
const skip = (page - 1) * limit;
const where = {};
if (role) where.role = role;
if (isActive !== undefined) where.isActive = isActive === 'true';
if (search) {
where.OR = [
{ email: { contains: search, mode: 'insensitive' } },
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ username: { contains: search, mode: 'insensitive' } },
];
}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isVerified: true,
isActive: true,
createdAt: true,
lastLoginAt: true,
},
orderBy: { createdAt: 'desc' },
skip: parseInt(skip),
take: parseInt(limit),
}),
prisma.user.count({ where }),
]);
// res.json({
// success: true,
// data: { users, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Users fetched successfully',
data: {
users,
pagination: {
page: +page,
limit: +limit,
total,
pages: Math.ceil(total / limit),
},
},
});
} catch (error) {
next(error);
}
};
exports.updateUserStatus = async (req, res, next) => {
try {
const { isActive, role } = req.body;
const { id } = req.params;
const user = await prisma.user.findUnique({ where: { id } });
// if (!user) return res.status(404).json({ success: false, message: 'User not found' });
if (!user) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'User not found',
});
}
const updatedUser = await prisma.user.update({
where: { id },
data: {
...(isActive !== undefined && { isActive }),
...(role && { role }),
},
});
// res.json({ success: true, message: 'User updated successfully', data: updatedUser });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'User updated successfully',
data: updatedUser,
});
} catch (error) {
next(error);
}
};
exports.getUserById = async (req, res, next) => {
try {
const { id } = req.params;
// if (!id) {
// return res.status(400).json({
// success: false,
// message: "User ID is required",
// });
// }
if (!id) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'User ID is required',
});
}
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isVerified: true,
isActive: true,
createdAt: true,
lastLoginAt: true,
},
});
// if (!user) {
// return res.status(404).json({
// success: false,
// message: "User not found",
// });
// }
if (!user) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'User not found',
});
}
// res.json({ success: true, data: { user } });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'User details fetched successfully',
data: { user },
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,297 @@
const authService = require('../services/authService');
// @desc Register user
exports.register = async (req, res, next) => {
try {
const { email, password, firstName, lastName, username, phone, role } =
req.body;
if (!email || !password) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Email and password are required',
});
}
if (password.length < 6) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Password must be at least 6 characters',
});
}
const result = await authService.register({
email,
password,
firstName,
lastName,
username,
phone,
role,
});
return res.status(201).json({
statusCode: 201,
status: true,
message: 'User registered successfully',
data: result,
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Login user
exports.login = async (req, res, next) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Email and password are required',
});
}
const result = await authService.login(email, password);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Login successful',
data: result,
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Refresh token
// @desc Refresh token
exports.refreshToken = async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Refresh token is required',
});
}
const result = await authService.refreshToken(refreshToken);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Token refreshed successfully',
data: result,
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Logout user
exports.logout = async (req, res, next) => {
try {
await authService.logout(req.user.id);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Logout successful',
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Change password
exports.changePassword = async (req, res, next) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Current password and new password are required',
});
}
if (newPassword.length < 6) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'New password must be at least 6 characters',
});
}
await authService.changePassword(req.user.id, currentPassword, newPassword);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Password changed successfully',
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Forgot password
exports.forgotPassword = async (req, res, next) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Email is required',
});
}
const result = await authService.requestPasswordReset(email);
return res.status(200).json({
statusCode: 200,
status: true,
message: result.message || 'Password reset email sent successfully',
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Reset password
exports.resetPassword = async (req, res, next) => {
try {
const { token, newPassword } = req.body;
if (!token || !newPassword) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Token and new password are required',
});
}
if (newPassword.length < 6) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Password must be at least 6 characters',
});
}
await authService.resetPassword(token, newPassword);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Password reset successfully',
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Send verification email
exports.sendVerification = async (req, res, next) => {
try {
await authService.sendVerificationEmail(req.user.id);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Verification email sent',
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Verify email
exports.verifyEmail = async (req, res, next) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Verification token is required',
});
}
await authService.verifyEmail(token);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Email verified successfully',
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};
// @desc Get current user profile
exports.getMe = async (req, res, next) => {
try {
return res.status(200).json({
statusCode: 200,
status: true,
message: 'User retrieved successfully',
data: { user: req.user },
});
} catch (error) {
return res.status(500).json({
statusCode: 500,
status: false,
message: error.message || 'Internal server error',
});
}
};

View File

@@ -0,0 +1,351 @@
const { prisma } = require('../config/database');
const { Decimal } = require('@prisma/client/runtime/library');
/**
* @desc Validate and apply coupon code
* @route POST /api/coupons/validate
* @access Public/Private
*/
exports.validateCoupon = async (req, res, next) => {
try {
const { code, orderAmount } = req.body;
const userId = req.user?.id;
if (!code || !orderAmount) {
return res.status(400).json({
status: false,
message: 'Coupon code and order amount are required',
});
}
// Find coupon
const coupon = await prisma.coupon.findUnique({
where: { code: code.toUpperCase() },
});
if (!coupon) {
return res.status(404).json({
status: false,
message: 'Invalid coupon code',
});
}
// Validate coupon
const validation = validateCouponRules(coupon, orderAmount);
if (!validation.isValid) {
return res.status(400).json({
status: false,
message: validation.message,
});
}
// Calculate discount
const discountAmount = calculateDiscount(coupon, orderAmount);
const finalAmount = orderAmount - discountAmount;
res.status(200).json({
status: true,
message: 'Coupon applied successfully',
data: {
couponCode: coupon.code,
couponType: coupon.type,
discountAmount: parseFloat(discountAmount.toFixed(2)),
originalAmount: parseFloat(orderAmount.toFixed(2)),
finalAmount: parseFloat(finalAmount.toFixed(2)),
savings: parseFloat(discountAmount.toFixed(2)),
savingsPercentage: parseFloat(((discountAmount / orderAmount) * 100).toFixed(2)),
},
});
} catch (error) {
console.error('Validate coupon error:', error);
next(error);
}
};
/**
* @desc Get available coupons for user
* @route GET /api/coupons/available
* @access Public
*/
exports.getAvailableCoupons = async (req, res, next) => {
try {
const { orderAmount } = req.query;
const now = new Date();
// Get active coupons
const coupons = await prisma.coupon.findMany({
where: {
isActive: true,
validFrom: { lte: now },
validUntil: { gte: now },
OR: [
{ maxUses: null }, // No limit
{ usedCount: { lt: prisma.coupon.fields.maxUses } }, // Has remaining uses
],
},
orderBy: { createdAt: 'desc' },
});
// Filter by minimum order amount if provided
let filteredCoupons = coupons;
if (orderAmount) {
filteredCoupons = coupons.filter(coupon => {
if (!coupon.minOrderAmount) return true;
return parseFloat(orderAmount) >= parseFloat(coupon.minOrderAmount);
});
}
// Add discount preview
const couponsWithPreview = filteredCoupons.map(coupon => {
let discountPreview = '';
if (coupon.type === 'PERCENTAGE') {
discountPreview = `${coupon.value}% OFF`;
} else if (coupon.type === 'FIXED_AMOUNT') {
discountPreview = `${coupon.value} OFF`;
} else if (coupon.type === 'FREE_SHIPPING') {
discountPreview = 'FREE SHIPPING';
}
return {
code: coupon.code,
description: coupon.description,
type: coupon.type,
value: parseFloat(coupon.value),
minOrderAmount: coupon.minOrderAmount ? parseFloat(coupon.minOrderAmount) : null,
validUntil: coupon.validUntil,
discountPreview,
remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null,
};
});
res.status(200).json({
status: true,
message: 'Available coupons fetched successfully',
data: couponsWithPreview,
});
} catch (error) {
next(error);
}
};
/**
* @desc Apply coupon to order
* @route POST /api/coupons/apply
* @access Private
*/
exports.applyCouponToOrder = async (req, res, next) => {
try {
const { couponCode, orderId } = req.body;
const userId = req.user.id;
// Get order
const order = await prisma.order.findUnique({
where: { id: orderId },
});
if (!order) {
return res.status(404).json({
status: false,
message: 'Order not found',
});
}
// Verify order belongs to user
if (order.userId !== userId) {
return res.status(403).json({
status: false,
message: 'Unauthorized',
});
}
// Check if order already has a coupon
if (order.discountAmount > 0) {
return res.status(400).json({
status: false,
message: 'Order already has a discount applied',
});
}
// Get coupon
const coupon = await prisma.coupon.findUnique({
where: { code: couponCode.toUpperCase() },
});
if (!coupon) {
return res.status(404).json({
status: false,
message: 'Invalid coupon code',
});
}
// Validate coupon
const orderAmount = parseFloat(order.subtotal);
const validation = validateCouponRules(coupon, orderAmount);
if (!validation.isValid) {
return res.status(400).json({
status: false,
message: validation.message,
});
}
// Calculate discount
let discountAmount = calculateDiscount(coupon, orderAmount);
let shippingAmount = parseFloat(order.shippingAmount);
// If free shipping coupon, set shipping to 0
if (coupon.type === 'FREE_SHIPPING') {
discountAmount = shippingAmount;
shippingAmount = 0;
}
// Recalculate total
const newTotal = orderAmount + parseFloat(order.taxAmount) + shippingAmount - discountAmount;
// Update order
const updatedOrder = await prisma.$transaction([
// Update order
prisma.order.update({
where: { id: orderId },
data: {
discountAmount,
shippingAmount,
totalAmount: newTotal,
},
}),
// Increment coupon usage
prisma.coupon.update({
where: { id: coupon.id },
data: {
usedCount: { increment: 1 },
},
}),
]);
res.status(200).json({
status: true,
message: 'Coupon applied successfully',
data: updatedOrder[0],
});
} catch (error) {
console.error('Apply coupon error:', error);
next(error);
}
};
/**
* @desc Remove coupon from order
* @route POST /api/coupons/remove
* @access Private
*/
exports.removeCouponFromOrder = async (req, res, next) => {
try {
const { orderId } = req.body;
const userId = req.user.id;
const order = await prisma.order.findUnique({
where: { id: orderId },
});
if (!order) {
return res.status(404).json({
status: false,
message: 'Order not found',
});
}
if (order.userId !== userId) {
return res.status(403).json({
status: false,
message: 'Unauthorized',
});
}
if (order.discountAmount === 0) {
return res.status(400).json({
status: false,
message: 'No coupon applied to this order',
});
}
// Recalculate total without discount
const newTotal =
parseFloat(order.subtotal) +
parseFloat(order.taxAmount) +
parseFloat(order.shippingAmount);
const updatedOrder = await prisma.order.update({
where: { id: orderId },
data: {
discountAmount: 0,
totalAmount: newTotal,
},
});
res.status(200).json({
status: true,
message: 'Coupon removed successfully',
data: updatedOrder,
});
} catch (error) {
next(error);
}
};
// ==========================================
// HELPER FUNCTIONS
// ==========================================
/**
* Validate coupon rules
*/
function validateCouponRules(coupon, orderAmount) {
const now = new Date();
// Check if active
if (!coupon.isActive) {
return { isValid: false, message: 'This coupon is no longer active' };
}
// Check date range
if (now < new Date(coupon.validFrom)) {
return { isValid: false, message: 'This coupon is not yet valid' };
}
if (now > new Date(coupon.validUntil)) {
return { isValid: false, message: 'This coupon has expired' };
}
// Check usage limit
if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) {
return { isValid: false, message: 'This coupon has reached its usage limit' };
}
// Check minimum order amount
if (coupon.minOrderAmount && orderAmount < parseFloat(coupon.minOrderAmount)) {
return {
isValid: false,
message: `Minimum order amount of ₹${coupon.minOrderAmount} required`,
};
}
return { isValid: true };
}
/**
* Calculate discount amount
*/
function calculateDiscount(coupon, orderAmount) {
if (coupon.type === 'PERCENTAGE') {
return (orderAmount * parseFloat(coupon.value)) / 100;
} else if (coupon.type === 'FIXED_AMOUNT') {
return Math.min(parseFloat(coupon.value), orderAmount);
} else if (coupon.type === 'FREE_SHIPPING') {
return 0; // Handled separately in order
}
return 0;
}

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

View File

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

View File

@@ -0,0 +1,430 @@
// controllers/paytmController.js
const { prisma } = require('../../config/database');
const {
initiateTransaction,
checkTransactionStatus,
verifyChecksum,
processRefund,
PaytmConfig,
} = require('../../utils/paytm');
/**
* @desc Initiate Paytm Payment
* @route POST /api/payments/paytm/initiate
* @access Private
*/
exports.initiatePayment = async (req, res, next) => {
try {
const { orderId, amount } = req.body;
const userId = req.user.id;
// Validate order
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: true,
},
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found',
});
}
// Verify order belongs to user
if (order.userId !== userId) {
return res.status(403).json({
success: false,
message: 'Unauthorized access to order',
});
}
// Check if order is already paid
if (order.paymentStatus === 'PAID') {
return res.status(400).json({
success: false,
message: 'Order is already paid',
});
}
// Validate amount
const orderAmount = parseFloat(order.totalAmount);
if (parseFloat(amount) !== orderAmount) {
return res.status(400).json({
success: false,
message: 'Payment amount mismatch',
});
}
// Initiate Paytm transaction
const paytmResponse = await initiateTransaction(
order.orderNumber,
orderAmount,
userId,
order.user.email,
order.user.phone || '9999999999'
);
if (!paytmResponse.success) {
return res.status(400).json({
success: false,
message: 'Failed to initiate payment',
error: paytmResponse,
});
}
// Update order with payment details
await prisma.order.update({
where: { id: orderId },
data: {
paymentMethod: 'PAYTM',
updatedAt: new Date(),
},
});
res.status(200).json({
success: true,
message: 'Payment initiated successfully',
data: {
txnToken: paytmResponse.txnToken,
orderId: order.orderNumber,
amount: orderAmount,
mid: PaytmConfig.mid,
website: PaytmConfig.website,
callbackUrl: PaytmConfig.callbackUrl,
},
});
} catch (error) {
console.error('Paytm initiate payment error:', error);
next(error);
}
};
/**
* @desc Paytm Payment Callback
* @route POST /api/payments/paytm/callback
* @access Public (Called by Paytm)
*/
exports.paymentCallback = async (req, res, next) => {
try {
const paytmChecksum = req.body.CHECKSUMHASH;
delete req.body.CHECKSUMHASH;
// Verify checksum
const isValidChecksum = await verifyChecksum(
req.body,
PaytmConfig.key,
paytmChecksum
);
if (!isValidChecksum) {
console.error('Invalid checksum received from Paytm');
return res.redirect(`${process.env.FRONTEND_URL}/payment/failed?reason=invalid_checksum`);
}
const {
ORDERID,
TXNID,
TXNAMOUNT,
STATUS,
RESPCODE,
RESPMSG,
TXNDATE,
BANKTXNID,
GATEWAYNAME,
} = req.body;
console.log('Paytm callback received:', {
orderId: ORDERID,
txnId: TXNID,
status: STATUS,
amount: TXNAMOUNT,
});
// Find order by order number
const order = await prisma.order.findUnique({
where: { orderNumber: ORDERID },
});
if (!order) {
console.error('Order not found:', ORDERID);
return res.redirect(`${process.env.FRONTEND_URL}/payment/failed?reason=order_not_found`);
}
// Update order based on payment status
if (STATUS === 'TXN_SUCCESS') {
await prisma.order.update({
where: { id: order.id },
data: {
paymentStatus: 'PAID',
paymentId: TXNID,
paymentMethod: `PAYTM - ${GATEWAYNAME || 'Gateway'}`,
status: 'CONFIRMED',
updatedAt: new Date(),
},
});
// Redirect to success page
return res.redirect(
`${process.env.FRONTEND_URL}/payment/success?orderId=${order.id}&txnId=${TXNID}`
);
} else if (STATUS === 'TXN_FAILURE') {
await prisma.order.update({
where: { id: order.id },
data: {
paymentStatus: 'FAILED',
paymentId: TXNID,
updatedAt: new Date(),
},
});
return res.redirect(
`${process.env.FRONTEND_URL}/payment/failed?orderId=${order.id}&reason=${RESPMSG}`
);
} else {
// Pending or other status
return res.redirect(
`${process.env.FRONTEND_URL}/payment/pending?orderId=${order.id}`
);
}
} catch (error) {
console.error('Paytm callback error:', error);
return res.redirect(
`${process.env.FRONTEND_URL}/payment/failed?reason=processing_error`
);
}
};
/**
* @desc Check Payment Status
* @route GET /api/payments/paytm/status/:orderId
* @access Private
*/
exports.checkPaymentStatus = async (req, res, next) => {
try {
const { orderId } = req.params;
const userId = req.user.id;
// Get order
const order = await prisma.order.findUnique({
where: { id: orderId },
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found',
});
}
// Verify order belongs to user
if (order.userId !== userId) {
return res.status(403).json({
success: false,
message: 'Unauthorized access',
});
}
// Check status with Paytm
const statusResponse = await checkTransactionStatus(order.orderNumber);
if (statusResponse.body && statusResponse.body.resultInfo) {
const { resultStatus } = statusResponse.body.resultInfo;
const txnInfo = statusResponse.body;
// Update order if status changed
if (resultStatus === 'TXN_SUCCESS' && order.paymentStatus !== 'PAID') {
await prisma.order.update({
where: { id: orderId },
data: {
paymentStatus: 'PAID',
paymentId: txnInfo.txnId,
status: 'CONFIRMED',
updatedAt: new Date(),
},
});
}
return res.status(200).json({
success: true,
data: {
orderId: order.id,
orderNumber: order.orderNumber,
paymentStatus: order.paymentStatus,
paytmStatus: resultStatus,
txnInfo: txnInfo,
},
});
}
res.status(200).json({
success: true,
data: {
orderId: order.id,
orderNumber: order.orderNumber,
paymentStatus: order.paymentStatus,
},
});
} catch (error) {
console.error('Check payment status error:', error);
next(error);
}
};
/**
* @desc Process Refund
* @route POST /api/payments/paytm/refund
* @access Private/Admin
*/
exports.processRefund = async (req, res, next) => {
try {
const { orderId, amount, reason } = req.body;
// Get order
const order = await prisma.order.findUnique({
where: { id: orderId },
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found',
});
}
// Validate refund amount
const orderAmount = parseFloat(order.totalAmount);
const refundAmount = parseFloat(amount);
if (refundAmount > orderAmount) {
return res.status(400).json({
success: false,
message: 'Refund amount cannot exceed order amount',
});
}
// Check if order is paid
if (order.paymentStatus !== 'PAID') {
return res.status(400).json({
success: false,
message: 'Cannot refund unpaid order',
});
}
// Generate unique refund ID
const refId = `REFUND_${order.orderNumber}_${Date.now()}`;
// Process refund with Paytm
const refundResponse = await processRefund(
order.orderNumber,
refId,
order.paymentId,
refundAmount
);
if (refundResponse.body && refundResponse.body.resultInfo) {
const { resultStatus, resultMsg } = refundResponse.body.resultInfo;
if (resultStatus === 'TXN_SUCCESS' || resultStatus === 'PENDING') {
// Update order
const isFullRefund = refundAmount >= orderAmount;
await prisma.order.update({
where: { id: orderId },
data: {
paymentStatus: isFullRefund ? 'REFUNDED' : 'PARTIALLY_REFUNDED',
status: isFullRefund ? 'REFUNDED' : order.status,
returnStatus: 'COMPLETED',
updatedAt: new Date(),
},
});
return res.status(200).json({
success: true,
message: 'Refund processed successfully',
data: {
refId,
status: resultStatus,
message: resultMsg,
amount: refundAmount,
},
});
} else {
return res.status(400).json({
success: false,
message: `Refund failed: ${resultMsg}`,
data: refundResponse,
});
}
}
res.status(500).json({
success: false,
message: 'Failed to process refund',
});
} catch (error) {
console.error('Process refund error:', error);
next(error);
}
};
/**
* @desc Get Payment Details
* @route GET /api/payments/paytm/:orderId
* @access Private
*/
exports.getPaymentDetails = async (req, res, next) => {
try {
const { orderId } = req.params;
const userId = req.user.id;
const order = await prisma.order.findUnique({
where: { id: orderId },
include: {
user: {
select: {
id: true,
email: true,
firstName: true,
lastName: true,
},
},
},
});
if (!order) {
return res.status(404).json({
success: false,
message: 'Order not found',
});
}
// Verify access
if (order.userId !== userId && req.user.role !== 'ADMIN') {
return res.status(403).json({
success: false,
message: 'Unauthorized access',
});
}
res.status(200).json({
success: true,
data: {
orderId: order.id,
orderNumber: order.orderNumber,
totalAmount: order.totalAmount,
paymentStatus: order.paymentStatus,
paymentMethod: order.paymentMethod,
paymentId: order.paymentId,
createdAt: order.createdAt,
user: order.user,
},
});
} catch (error) {
console.error('Get payment details error:', error);
next(error);
}
};

View File

@@ -0,0 +1,811 @@
const Product = require('../../models/mongodb/Product');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Get all products
exports.getAllProducts = async (req, res, next) => {
try {
const {
page = 1,
limit = 20,
category,
brand,
minPrice,
maxPrice,
search,
sort = 'createdAt',
order = 'desc',
} = req.query;
const skip = (page - 1) * limit;
let query = { status: 'active' };
if (category) query.category = category;
if (brand) query.brand = brand;
if (minPrice || maxPrice) {
query.basePrice = {};
if (minPrice) query.basePrice.$gte = parseFloat(minPrice);
if (maxPrice) query.basePrice.$lte = parseFloat(maxPrice);
}
if (search) query.$text = { $search: search };
const sortOptions = {};
sortOptions[sort] = order === 'desc' ? -1 : 1;
const [products, total] = await Promise.all([
Product.find(query)
.sort(sortOptions)
.skip(skip)
.limit(parseInt(limit))
.lean(),
Product.countDocuments(query),
]);
// res.json({
// success: true,
// data: {
// products,
// pagination: {
// page: parseInt(page),
// limit: parseInt(limit),
// total,
// pages: Math.ceil(total / limit),
// },
// },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Products fetched successfully',
data: {
products,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit),
},
},
});
} catch (error) {
next(error);
}
};
// Get single product by slug
exports.getProductBySlug = async (req, res, next) => {
try {
const product = await Product.findBySlug(req.params.slug);
// if (!product)
// return res
// .status(404)
// .json({ success: false, message: 'Product not found' });
if (!product) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Product not found',
});
}
await product.incrementViewCount();
// res.json({ success: true, data: { product } });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Product fetched successfully',
data: { product },
});
} catch (error) {
next(error);
}
};
// Create new product
exports.createProduct = async (req, res, next) => {
try {
const product = new Product(req.body);
await product.save();
// res.status(201).json({
// success: true,
// message: 'Product created successfully',
// data: { product },
// });
return res.status(201).json({
statusCode: 201,
status: true,
message: 'Product created successfully',
data: { product },
});
} catch (error) {
next(error);
}
};
// Update product
exports.updateProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true,
});
// if (!product)
// return res
// .status(404)
// .json({ success: false, message: 'Product not found' });
if (!product) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Product not found',
});
}
// res.json({
// success: true,
// message: 'Product updated successfully',
// data: { product },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Product updated successfully',
data: { product },
});
} catch (error) {
next(error);
}
};
// Delete product
exports.deleteProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
// if (!product)
// return res
// .status(404)
// .json({ success: false, message: 'Product not found' });
if (!product) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Product not found',
});
}
// res.json({ success: true, message: 'Product deleted successfully' });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Product deleted successfully',
});
} catch (error) {
next(error);
}
};
// Search products
exports.searchProducts = async (req, res, next) => {
try {
const { query } = req.params;
const {
category,
brand,
minPrice,
maxPrice,
limit = 20,
skip = 0,
} = req.query;
const products = await Product.searchProducts(query, {
category,
brand,
minPrice: minPrice ? parseFloat(minPrice) : undefined,
maxPrice: maxPrice ? parseFloat(maxPrice) : undefined,
limit: parseInt(limit),
skip: parseInt(skip),
});
// res.json({ success: true, data: { products } });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Products searched successfully',
data: { products },
});
} catch (error) {
next(error);
}
};
// Get products by category
// exports.getProductsByCategory = async (req, res, next) => {
// try {
// const { category } = req.params;
// const { limit = 20, skip = 0 } = req.query;
// const products = await Product.findByCategory(
// category,
// parseInt(limit),
// parseInt(skip)
// );
// // res.json({ success: true, data: { products } });
// return res.status(200).json({
// statusCode: 200,
// status: true,
// message: 'Products fetched by category',
// data: { products },
// });
// } catch (error) {
// next(error);
// }
// };
// exports.getProductsByCategory = async (req, res, next) => {
// try {
// const { category: slug } = req.params;
// const { limit = 20, skip = 0 } = req.query;
// // 1⃣ Get category from PostgreSQL
// const categoryDoc = await prisma.category.findFirst({
// where: { slug, isActive: true },
// });
// if (!categoryDoc) {
// return res.status(404).json({
// statusCode: 404,
// status: false,
// message: "Category not found",
// data: null
// });
// }
// // 2⃣ Get products from MongoDB using the PostgreSQL category ID
// const products = await Product.findByCategory(
// categoryDoc.id, // <-- MongoDB stores this as the `category` field
// parseInt(limit),
// parseInt(skip)
// );
// return res.status(200).json({
// statusCode: 200,
// status: true,
// message: "Products fetched by category",
// data: { products },
// });
// } catch (error) {
// next(error);
// }
// };
// exports.getProductsByCategory = async (req, res, next) => {
// try {
// const { categorySlug } = req.params;
// const limit = parseInt(req.query.limit) || 20;
// const skip = parseInt(req.query.skip) || 0;
// // 1⃣ Find category from PostgreSQL by slug
// const categoryDoc = await prisma.category.findFirst({
// where: { slug: categorySlug, isActive: true },
// });
// if (!categoryDoc) {
// return res.status(404).json({
// statusCode: 404,
// status: false,
// message: 'Category not found',
// data: null,
// });
// }
// // 2⃣ Find products from MongoDB using the PostgreSQL category ID
// const query = { category: categoryDoc.id }; // MongoDB field must store PostgreSQL category id
// const [products, totalCount] = await Promise.all([
// Product.find(query)
// .skip(skip)
// .limit(limit)
// .sort({ createdAt: -1 }) // optional: newest first
// .lean(), // optional: return plain JS objects
// Product.countDocuments(query),
// ]);
// return res.status(200).json({
// statusCode: 200,
// status: true,
// message: 'Products fetched by category',
// data: { products, totalCount },
// });
// } catch (error) {
// next(error);
// }
// };
exports.getProductsByCategory = async (req, res, next) => {
try {
const { categorySlug } = req.params;
const limit = parseInt(req.query.limit) || 20;
const skip = parseInt(req.query.skip) || 0;
// 1⃣ Find category from PostgreSQL
const categoryDoc = await prisma.category.findFirst({
where: { slug: categorySlug, isActive: true },
});
if (!categoryDoc) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Category not found',
data: { products: [], totalCount: 0 },
});
}
// 2⃣ Find all child categories recursively (if any)
const allCategoryIds = [categoryDoc.id];
const getChildCategoryIds = async (parentId) => {
const children = await prisma.category.findMany({
where: { parentId, isActive: true },
select: { id: true },
});
for (const child of children) {
allCategoryIds.push(child.id);
await getChildCategoryIds(child.id);
}
};
await getChildCategoryIds(categoryDoc.id);
// 3⃣ Find products in MongoDB with category in allCategoryIds
const query = { category: { $in: allCategoryIds } };
const [products, totalCount] = await Promise.all([
Product.find(query)
.skip(skip)
.limit(limit)
.sort({ createdAt: -1 })
.lean(),
Product.countDocuments(query),
]);
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Products fetched by category',
data: { products, totalCount },
});
} catch (error) {
next(error);
}
};
// Get featured products
exports.getFeaturedProducts = async (req, res, next) => {
try {
const { limit = 10 } = req.query;
const products = await Product.find({ status: 'active', isFeatured: true })
.sort({ createdAt: -1 })
.limit(parseInt(limit))
.lean();
// res.json({ success: true, data: { products } });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Featured products fetched successfully',
data: { products },
});
} catch (error) {
next(error);
}
};
// Get all unique categories
// Get all categories that have products
exports.getAllCategories = async (req, res) => {
try {
// Get unique category IDs from MongoDB products
const categoryIds = await Product.distinct('category', {
status: 'active',
});
console.log('Category IDs from products:', categoryIds);
if (!categoryIds || categoryIds.length === 0) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'No categories found',
});
}
// Fetch category details from PostgreSQL
const categories = await prisma.category.findMany({
where: {
id: { in: categoryIds },
isActive: true,
},
select: {
id: true,
name: true,
slug: true,
image: true,
description: true,
},
orderBy: {
name: 'asc',
},
});
if (!categories || categories.length === 0) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'No matching categories found in database',
});
}
// Get product count for each category
const productCounts = await Product.aggregate([
{
$match: {
category: { $in: categoryIds },
status: 'active',
},
},
{
$group: {
_id: '$category',
count: { $sum: 1 },
},
},
]);
// Add product count to each category
const categoriesWithCount = categories.map(cat => ({
...cat,
productCount: productCounts.find(pc => pc._id === cat.id)?.count || 0,
}));
console.log('Categories with counts:', categoriesWithCount);
return res.json({
statusCode: 200,
status: true,
data: categoriesWithCount,
});
} catch (error) {
console.error('Error:', error);
return res.status(500).json({
statusCode: 500,
status: false,
message: 'Server error',
error: error.message,
});
}
};
exports.debugMissingCategories = async (req, res) => {
try {
const categoryIds = await Product.distinct('category');
const existingCategories = await prisma.category.findMany({
where: { id: { in: categoryIds } },
select: { id: true, name: true },
});
const existingIds = existingCategories.map(c => c.id);
const missingIds = categoryIds.filter(id => !existingIds.includes(id));
return res.json({
totalProductCategories: categoryIds.length,
existingInDB: existingCategories,
missingFromDB: missingIds,
});
} catch (error) {
return res.status(500).json({ error: error.message });
}
};
// exports.getUserCategoryHierarchy = async (req, res, next) => {
// try {
// const categories = await prisma.category.findMany({
// where: {
// isActive: true,
// // isVisible: true,
// },
// select: {
// id: true,
// name: true,
// slug: true,
// parentId: true,
// },
// orderBy: { name: 'asc' },
// });
// const lookup = {};
// categories.forEach(cat => {
// lookup[cat.id] = { ...cat, children: [] };
// });
// const hierarchy = [];
// categories.forEach(cat => {
// if (cat.parentId && lookup[cat.parentId]) {
// lookup[cat.parentId].children.push(lookup[cat.id]);
// } else {
// hierarchy.push(lookup[cat.id]);
// }
// });
// res.status(200).json({
// statusCode: 200,
// status: true,
// message: 'Category tree fetched successfully',
// data: hierarchy,
// });
// } catch (error) {
// next(error);
// }
// };
exports.getUserCategoryHierarchy = async (req, res, next) => {
try {
const categories = await prisma.category.findMany({
where: {
isActive: true,
},
select: {
id: true,
name: true,
slug: true,
parentId: true,
sequence: true,
},
orderBy: {
sequence: 'asc', // ✅ IMPORTANT
},
});
const lookup = {};
categories.forEach(cat => {
lookup[cat.id] = { ...cat, children: [] };
});
const hierarchy = [];
categories.forEach(cat => {
if (cat.parentId && lookup[cat.parentId]) {
lookup[cat.parentId].children.push(lookup[cat.id]);
} else {
hierarchy.push(lookup[cat.id]);
}
});
// ✅ Recursive sort by sequence
const sortTree = nodes => {
nodes.sort((a, b) => a.sequence - b.sequence);
nodes.forEach(node => {
if (node.children.length) {
sortTree(node.children);
}
});
};
sortTree(hierarchy);
res.status(200).json({
statusCode: 200,
status: true,
message: 'Category tree fetched successfully',
data: hierarchy,
});
} catch (error) {
next(error);
}
};
// Get all available colors for a category
// const Product = require('../../models/mongodb/Product');
// controllers/products/productController.js
exports.getCategoryColors = async (req, res) => {
try {
const { categorySlug } = req.params;
// 1⃣ Find category ID from Prisma (PostgreSQL)
const category = await prisma.category.findFirst({
where: { slug: categorySlug },
select: { id: true, name: true },
});
if (!category) {
return res.json({
status: false,
message: 'Category not found',
data: [],
});
}
// 2⃣ Fetch products from MongoDB using category ID
const products = await Product.find(
{ category: category.id, status: 'active' },
{ variants: 1, images: 1 }
);
if (!products.length) {
return res.json({
status: true,
message: 'No products found in this category',
data: [],
});
}
// 3⃣ Extract unique colors
const colorMap = new Map();
products.forEach((product) => {
if (product.variants?.length) {
product.variants.forEach((variant) => {
if (!variant.color) return; // skip if no color
const key = variant.color.toLowerCase();
if (!colorMap.has(key)) {
colorMap.set(key, {
name: variant.color,
slug: key,
image: variant.images?.[0] || product.images?.primary || null,
bg: getColorBg(key), // Optional: your helper to generate color background
});
}
});
}
});
// 4⃣ Prepare response
const colors = Array.from(colorMap.values());
const message =
colors.length === 0
? 'No color variants available for this category'
: colors.length === 1
? `Only 1 color available: ${colors[0].name}`
: 'Category colors fetched successfully';
res.json({
status: true,
message,
data: colors,
});
} catch (error) {
console.error('Category Colors Error:', error);
res.status(500).json({
status: false,
message: 'Failed to fetch category colors',
data: [],
});
}
};
// Helper function to assign background color (optional)
function getColorBg(colorKey) {
const defaultBg = '#F5F5F5';
const colorMap = {
white: '#FFFFFF',
black: '#000000',
red: '#FF0000',
blue: '#007BFF',
green: '#28A745',
yellow: '#FFC107',
// Add more colors if needed
};
return colorMap[colorKey] || defaultBg;
}
/* helpers */
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
function getColorBg(color) {
const map = {
white: '#F5F5F5',
black: '#E5E5E5',
red: '#FDE6E6',
blue: '#E6F0FD',
green: '#E6FDEB',
pink: '#FDE6F1',
maroon: '#FDE6EB',
beige: '#FDEDE6',
grey: '#EFEFEF',
gray: '#EFEFEF',
brown: '#EEE6D8',
yellow: '#FFF7CC',
orange: '#FFE5CC',
purple: '#F1E6FD',
};
return map[color] || '#F3F4F6';
}
/**
* @desc Get New Arrivals
* @route GET /api/products/new-arrivals
* @access Public
*/
exports.getNewArrivals = async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 10;
// Fetch latest active products
const products = await Product.find({ status: 'active' })
.sort({ createdAt: -1 }) // newest first
.limit(limit);
res.status(200).json({ success: true, data: products });
} catch (error) {
console.error('Error fetching new arrivals:', error);
res.status(500).json({ success: false, message: 'Server Error' });
}
};
// controllers/products/productController.js
// @desc Get Most Loved / Discounted Products
// @route GET /api/products/most-loved
// @access Public
// Get Most Loved Products (based on purchaseCount or discounted items)
// @desc Get Most Loved / Discounted Products
// @route GET /api/products/most-loved
// @access Public
exports.getMostLovedProducts = async (req, res) => {
try {
const limit = parseInt(req.query.limit) || 8;
// Fetch products with discount or highest purchase count
let products = await Product.find({
status: 'active',
$or: [
{ compareAtPrice: { $gt: 0 } }, // discounted
{ purchaseCount: { $gt: 0 } }, // most loved
],
})
.sort({ purchaseCount: -1, createdAt: -1 })
.limit(limit);
// If no products match, return latest active products as fallback
if (!products.length) {
products = await Product.find({ status: 'active' })
.sort({ createdAt: -1 })
.limit(limit);
return res.status(200).json({
status: true,
message: 'Fallback products fetched successfully',
data: { products },
});
}
// Return the main products
return res.status(200).json({
status: true,
message: 'Products fetched successfully',
data: { products },
});
} catch (error) {
console.error('Error fetching most loved products:', error);
return res.status(500).json({ status: false, message: 'Server Error' });
}
};

View File

@@ -0,0 +1,345 @@
// controllers/products/recommendationController.js
const Product = require('../../models/mongodb/Product');
const { prisma } = require('../../config/database');
/**
* @desc Get recommendations for a product
* @route GET /api/products/:slug/recommendations
* @access Public
*/
const getProductRecommendations = async (req, res, next) => {
try {
const { slug } = req.params;
const { limit = 12 } = req.query;
// Get the current product
const currentProduct = await Product.findOne({ slug, status: 'active' });
if (!currentProduct) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}
// Build recommendation query
const recommendations = await getRecommendedProducts(
currentProduct,
parseInt(limit)
);
res.status(200).json({
success: true,
count: recommendations.length,
data: recommendations,
});
} catch (error) {
console.error('Get recommendations error:', error);
next(error);
}
};
/**
* @desc Get "Customers also bought" products
* @route GET /api/products/:slug/also-bought
* @access Public
*/
const getAlsoBoughtProducts = async (req, res, next) => {
try {
const { slug } = req.params;
const { limit = 8 } = req.query;
const currentProduct = await Product.findOne({ slug, status: 'active' });
if (!currentProduct) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}
// Get products frequently bought together
const alsoBought = await getFrequentlyBoughtTogether(
currentProduct._id.toString(),
parseInt(limit)
);
res.status(200).json({
success: true,
count: alsoBought.length,
data: alsoBought,
});
} catch (error) {
console.error('Get also bought error:', error);
next(error);
}
};
/**
* @desc Get similar products
* @route GET /api/products/:slug/similar
* @access Public
*/
const getSimilarProducts = async (req, res, next) => {
try {
const { slug } = req.params;
const { limit = 10 } = req.query;
const currentProduct = await Product.findOne({ slug, status: 'active' });
if (!currentProduct) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}
// Get similar products based on attributes
const similar = await getSimilarProductsByAttributes(
currentProduct,
parseInt(limit)
);
res.status(200).json({
success: true,
count: similar.length,
data: similar,
});
} catch (error) {
console.error('Get similar products error:', error);
next(error);
}
};
/**
* @desc Get personalized recommendations for user
* @route GET /api/products/recommendations/personalized
* @access Private
*/
const getPersonalizedRecommendations = async (req, res, next) => {
try {
const userId = req.user?.id;
const { limit = 20 } = req.query;
if (!userId) {
// Return popular products for non-authenticated users
const popularProducts = await Product.find({ status: 'active' })
.sort({ purchaseCount: -1, viewCount: -1 })
.limit(parseInt(limit));
return res.status(200).json({
success: true,
count: popularProducts.length,
data: popularProducts,
});
}
// Get user's purchase history
const userOrders = await prisma.order.findMany({
where: {
userId,
status: 'DELIVERED',
},
include: {
items: true,
},
take: 10,
orderBy: { createdAt: 'desc' },
});
// Get user's wishlist
const wishlist = await prisma.wishlistItem.findMany({
where: { userId },
take: 20,
});
// Extract product IDs
const purchasedProductIds = userOrders.flatMap((order) =>
order.items.map((item) => item.productId)
);
const wishlistProductIds = wishlist.map((item) => item.productId);
// Get categories and tags from purchased products
const purchasedProducts = await Product.find({
_id: { $in: purchasedProductIds },
});
const categories = [...new Set(purchasedProducts.map((p) => p.category))];
const tags = [
...new Set(purchasedProducts.flatMap((p) => p.tags || [])),
];
// Build personalized recommendations
const recommendations = await Product.find({
status: 'active',
_id: {
$nin: [...purchasedProductIds, ...wishlistProductIds],
},
$or: [
{ category: { $in: categories } },
{ tags: { $in: tags } },
{ isFeatured: true },
],
})
.sort({ purchaseCount: -1, viewCount: -1 })
.limit(parseInt(limit));
res.status(200).json({
success: true,
count: recommendations.length,
data: recommendations,
});
} catch (error) {
console.error('Get personalized recommendations error:', error);
next(error);
}
};
/**
* Helper Functions
*/
// Get recommended products based on multiple factors
async function getRecommendedProducts(currentProduct, limit) {
const priceRange = {
min: currentProduct.basePrice * 0.7,
max: currentProduct.basePrice * 1.3,
};
// Score-based recommendation
const recommendations = await Product.aggregate([
{
$match: {
_id: { $ne: currentProduct._id },
status: 'active',
},
},
{
$addFields: {
score: {
$add: [
{ $cond: [{ $eq: ['$category', currentProduct.category] }, 50, 0] },
{
$cond: [
{
$and: [
{ $gte: ['$basePrice', priceRange.min] },
{ $lte: ['$basePrice', priceRange.max] },
],
},
30,
0,
],
},
{
$multiply: [
{
$size: {
$ifNull: [
{
$setIntersection: [
{ $ifNull: ['$tags', []] },
currentProduct.tags || [],
],
},
[],
],
},
},
5,
],
},
{ $cond: ['$isFeatured', 20, 0] },
{ $divide: [{ $ifNull: ['$viewCount', 0] }, 100] },
{ $divide: [{ $multiply: [{ $ifNull: ['$purchaseCount', 0] }, 10] }, 10] },
],
},
},
},
{ $sort: { score: -1 } },
{ $limit: limit },
]);
return recommendations;
}
// Get frequently bought together products
async function getFrequentlyBoughtTogether(productId, limit) {
try {
const ordersWithProduct = await prisma.orderItem.findMany({
where: { productId },
select: { orderId: true },
distinct: ['orderId'],
});
if (ordersWithProduct.length === 0) {
const product = await Product.findById(productId);
return await Product.find({
_id: { $ne: productId },
category: product.category,
status: 'active',
})
.sort({ purchaseCount: -1 })
.limit(limit);
}
const orderIds = ordersWithProduct.map((item) => item.orderId);
const otherProducts = await prisma.orderItem.findMany({
where: {
orderId: { in: orderIds },
productId: { not: productId },
},
select: { productId: true },
});
const productFrequency = {};
otherProducts.forEach((item) => {
productFrequency[item.productId] =
(productFrequency[item.productId] || 0) + 1;
});
const sortedProductIds = Object.entries(productFrequency)
.sort(([, a], [, b]) => b - a)
.slice(0, limit)
.map(([id]) => id);
const products = await Product.find({
_id: { $in: sortedProductIds },
status: 'active',
});
return products;
} catch (error) {
console.error('Get frequently bought together error:', error);
return [];
}
}
// Get similar products by attributes
async function getSimilarProductsByAttributes(currentProduct, limit) {
const similar = await Product.find({
_id: { $ne: currentProduct._id },
status: 'active',
$or: [
{ category: currentProduct.category },
{ tags: { $in: currentProduct.tags || [] } },
{ brand: currentProduct.brand },
],
})
.sort({
purchaseCount: -1,
viewCount: -1,
})
.limit(limit);
return similar;
}
module.exports = {
getProductRecommendations,
getAlsoBoughtProducts,
getSimilarProducts,
getPersonalizedRecommendations,
};

View File

@@ -0,0 +1,128 @@
const { prisma } = require('../../config/database');
// Get addresses
exports.getAddresses = async (req, res, next) => {
try {
const addresses = await prisma.address.findMany({
where: { userId: req.user.id },
orderBy: { createdAt: 'desc' },
});
// res.json({ success: true, data: { addresses } });
return res.json({
statusCode: 200,
status: true,
message: 'Addresses fetched successfully',
data: { addresses },
});
} catch (error) {
next(error);
}
};
// Add address
exports.addAddress = async (req, res, next) => {
try {
const data = { ...req.body, userId: req.user.id };
if (data.isDefault) {
await prisma.address.updateMany({
where: { userId: req.user.id },
data: { isDefault: false },
});
}
const address = await prisma.address.create({ data });
// res.status(201).json({ success: true, message: 'Address added successfully', data: { address } });
return res.status(201).json({
statusCode: 201,
status: true,
message: 'Address added successfully',
data: { address },
});
} catch (error) {
next(error);
}
};
// Update address
exports.updateAddress = async (req, res, next) => {
try {
const { id } = req.params;
const existingAddress = await prisma.address.findFirst({
where: { id, userId: req.user.id },
});
// if (!existingAddress) {
// return res
// .status(404)
// .json({ success: false, message: 'Address not found' });
// }
if (!existingAddress) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Address not found',
});
}
if (req.body.isDefault) {
await prisma.address.updateMany({
where: { userId: req.user.id },
data: { isDefault: false },
});
}
const updatedAddress = await prisma.address.update({
where: { id },
data: req.body,
});
// res.json({
// success: true,
// message: 'Address updated successfully',
// data: { address: updatedAddress },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Address updated successfully',
data: { address: updatedAddress },
});
} catch (error) {
next(error);
}
};
// Delete address
exports.deleteAddress = async (req, res, next) => {
try {
const { id } = req.params;
const existing = await prisma.address.findFirst({
where: { id, userId: req.user.id },
});
// if (!existing) {
// return res
// .status(404)
// .json({ success: false, message: 'Address not found' });
// }
if (!existing) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Address not found',
});
}
await prisma.address.delete({ where: { id } });
// res.json({ success: true, message: 'Address deleted successfully' });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Address deleted successfully',
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,70 @@
const { prisma } = require('../../config/database');
exports.getAllUsers = async (req, res, next) => {
try {
const { page = 1, limit = 20, role, search } = req.query;
const skip = (page - 1) * limit;
const where = {};
if (role) where.role = role;
if (search) {
where.OR = [
{ email: { contains: search, mode: 'insensitive' } },
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ username: { contains: search, mode: 'insensitive' } },
];
}
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isVerified: true,
isActive: true,
createdAt: true,
lastLoginAt: true,
},
orderBy: { createdAt: 'desc' },
skip: parseInt(skip),
take: parseInt(limit),
}),
prisma.user.count({ where }),
]);
// res.json({
// success: true,
// data: {
// users,
// pagination: {
// page: parseInt(page),
// limit: parseInt(limit),
// total,
// pages: Math.ceil(total / limit),
// },
// },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Users fetched successfully",
data: {
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit),
},
},
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,150 @@
const mongoose = require("mongoose");
// const Product = require("../models/Product");
const Product = require('../../models/mongodb/Product');
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// Add to Cart
exports.addToCart = async (req, res, next) => {
try {
const { productId, quantity = 1 } = req.body;
if (!productId) {
return res.status(400).json({
status: false,
message: "Product ID is required",
});
}
// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(productId)) {
return res.status(400).json({
status: false,
message: "Invalid product ID",
});
}
// Check if product exists in MongoDB
const productExists = await Product.findById(productId);
if (!productExists) {
return res.status(404).json({
status: false,
message: "Product not found",
});
}
// Check if item already exists in cart
const existing = await prisma.cartItem.findUnique({
where: { userId_productId: { userId: req.user.id, productId } },
});
if (existing) {
// Update quantity
const updated = await prisma.cartItem.update({
where: { userId_productId: { userId: req.user.id, productId } },
data: { quantity: existing.quantity + quantity },
});
return res.status(200).json({
status: true,
message: "Cart quantity updated",
data: updated,
});
}
// Create new cart item
const item = await prisma.cartItem.create({
data: {
userId: req.user.id,
productId,
quantity,
},
});
return res.status(201).json({
status: true,
message: "Item added to cart",
data: item,
});
} catch (error) {
next(error);
}
};
//Get User Cart
exports.getCart = async (req, res, next) => {
try {
const cart = await prisma.cartItem.findMany({
where: { userId: req.user.id },
orderBy: { createdAt: "desc" },
});
// Fetch product details from MongoDB
const productIds = cart.map((item) => item.productId);
const products = await Product.find({ _id: { $in: productIds } });
// Merge product details into cart
const cartWithProducts = cart.map((item) => ({
...item,
product: products.find((p) => p._id.toString() === item.productId),
}));
return res.status(200).json({
status: true,
message: "Cart fetched successfully",
data: cartWithProducts,
});
} catch (error) {
next(error);
}
};
//Update Quantity
exports.updateQuantity = async (req, res, next) => {
try {
const { productId } = req.params;
const { quantity } = req.body;
if (!quantity || quantity < 1) {
return res.status(400).json({
status: false,
message: "Quantity must be at least 1",
});
}
const updated = await prisma.cartItem.update({
where: { userId_productId: { userId: req.user.id, productId } },
data: { quantity },
});
return res.status(200).json({
status: true,
message: "Cart quantity updated",
data: updated,
});
} catch (error) {
next(error);
}
};
//Remove From Cart
exports.removeFromCart = async (req, res, next) => {
try {
const { productId } = req.params;
await prisma.cartItem.delete({
where: { userId_productId: { userId: req.user.id, productId } },
});
return res.status(200).json({
status: true,
message: "Item removed from cart",
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,51 @@
const { prisma } = require('../../config/database');
exports.getOrders = 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);
}
};

View File

@@ -0,0 +1,113 @@
const { prisma } = require('../../config/database');
const uploadToS3 = require('../../utils/uploadToS3');
// Get user profile
exports.getProfile = async (req, res, next) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user.id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
phone: true,
avatar: true,
role: true,
isVerified: true,
createdAt: true,
lastLoginAt: true,
},
});
// res.json({ success: true, data: { user } });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Profile fetched successfully',
data: { user },
});
} catch (error) {
next(error);
}
};
// Update user profile
exports.updateProfile = async (req, res, next) => {
try {
const { firstName, lastName, username, phone } = req.body;
if (username) {
const existingUser = await prisma.user.findFirst({
where: { username, NOT: { id: req.user.id } },
});
// if (existingUser) {
// return res.status(400).json({ success: false, message: 'Username already taken' });
// }
if (existingUser) {
return res.status(400).json({
statusCode: 400,
status: false,
message: 'Username already taken',
});
}
}
const updatedUser = await prisma.user.update({
where: { id: req.user.id },
data: { firstName, lastName, username, phone },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
phone: true,
avatar: true,
role: true,
isVerified: true,
},
});
// res.json({ success: true, message: 'Profile updated successfully', data: { user: updatedUser } });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Profile updated successfully',
data: { user: updatedUser },
});
} catch (error) {
next(error);
}
};
// Upload avatar (profile picture)
exports.uploadAvatar = async (req, res, next) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: "No file uploaded",
});
}
const avatarUrl = req.file.location; // multer-s3 gives S3 URL
// Update avatar in Prisma
await prisma.user.update({
where: { id: req.user.id },
data: { avatar: avatarUrl },
});
res.status(200).json({
success: true,
message: "Profile picture uploaded successfully",
avatarUrl, // only return the profile picture link
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,245 @@
const { prisma } = require('../../config/database');
// import Product from '../../models/mongodb/Product';
const Product = require('../../models/mongodb/Product');
// exports.getWishlist = async (req, res, next) => {
// try {
// const wishlist = await prisma.wishlistItem.findMany({
// where: { userId: req.user.id },
// orderBy: { createdAt: 'desc' },
// });
// // Fetch product details from MongoDB
// const detailedWishlist = await Promise.all(
// wishlist.map(async item => {
// const product = await Product.findById(item.productId).select(
// 'name basePrice variants images'
// );
// return {
// ...item,
// product: product || null,
// };
// })
// );
// // res.json({ success: true, data: { wishlist } });
// return res.status(200).json({
// // statusCode: 200,
// status: true,
// message: 'Wishlist fetched successfully',
// // data: { wishlist },
// data: { wishlist: detailedWishlist },
// });
// } catch (error) {
// next(error);
// }
// };
// exports.addToWishlist = async (req, res, next) => {
// try {
// const { productId } = req.body;
// // if (!productId) return res.status(400).json({ success: false, message: 'Product ID is required' });
// if (!productId) {
// return res.status(400).json({
// statusCode: 400,
// status: false,
// message: 'Product ID is required',
// });
// }
// const existing = await prisma.wishlistItem.findUnique({
// where: { userId_productId: { userId: req.user.id, productId } },
// });
// // if (existing) return res.status(400).json({ success: false, message: 'Item already in wishlist' });
// if (existing) {
// return res.status(400).json({
// statusCode: 400,
// status: false,
// message: 'Item already in wishlist',
// });
// }
// const item = await prisma.wishlistItem.create({
// data: { userId: req.user.id, productId },
// });
// // res.status(201).json({ success: true, message: 'Item added to wishlist', data: { item } });
// return res.status(201).json({
// statusCode: 201,
// status: true,
// message: 'Item added to wishlist',
// data: { item },
// });
// } catch (error) {
// next(error);
// }
// };
const mongoose = require("mongoose");
exports.getWishlist = async (req, res, next) => {
try {
const wishlist = await prisma.wishlistItem.findMany({
where: { userId: req.user.id },
orderBy: { createdAt: "desc" },
});
const detailedWishlist = await Promise.all(
wishlist.map(async (item) => {
let product = null;
// Only try MongoDB lookup if valid ObjectId
if (mongoose.Types.ObjectId.isValid(item.productId)) {
product = await Product.findById(item.productId).select(
"name basePrice variants images"
);
}
return {
...item,
product: product || null,
};
})
);
return res.status(200).json({
status: true,
message: "Wishlist fetched successfully",
data: { wishlist: detailedWishlist },
});
} catch (error) {
next(error);
}
};
// exports.addToWishlist = async (req, res, next) => {
// try {
// const { productId } = req.body;
// if (!productId) {
// return res.status(400).json({
// statusCode: 400,
// status: false,
// message: 'Product ID is required',
// });
// }
// const existing = await prisma.wishlistItem.findUnique({
// where: { userId_productId: { userId: req.user.id, productId } },
// });
// if (existing) {
// return res.status(400).json({
// statusCode: 400,
// status: false,
// message: 'Item already in wishlist',
// });
// }
// const item = await prisma.wishlistItem.create({
// data: { userId: req.user.id, productId },
// });
// return res.status(201).json({
// statusCode: 201,
// status: true,
// message: 'Item added to wishlist',
// data: { item },
// });
// } catch (error) {
// next(error);
// }
// };
// const mongoose = require("mongoose");
exports.addToWishlist = async (req, res, next) => {
try {
const { productId } = req.body;
if (!productId) {
return res.status(400).json({
statusCode: 400,
status: false,
message: "Product ID is required",
});
}
// 1⃣ Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(productId)) {
return res.status(400).json({
status: false,
message: "Invalid product ID (must be MongoDB ObjectId)",
});
}
// 2⃣ Ensure product exists in MongoDB
const productExists = await Product.findById(productId);
if (!productExists) {
return res.status(404).json({
status: false,
message: "Product not found in database",
});
}
// 3⃣ Check duplicate
const existing = await prisma.wishlistItem.findUnique({
where: { userId_productId: { userId: req.user.id, productId } },
});
if (existing) {
return res.status(400).json({
status: false,
message: "Item already in wishlist",
});
}
// 4⃣ Save VALID productId in Prisma
const item = await prisma.wishlistItem.create({
data: { userId: req.user.id, productId },
});
return res.status(201).json({
status: true,
message: "Item added to wishlist",
data: { item },
});
} catch (error) {
next(error);
}
};
exports.removeFromWishlist = async (req, res, next) => {
try {
const { productId } = req.params;
const existing = await prisma.wishlistItem.findUnique({
where: { userId_productId: { userId: req.user.id, productId } },
});
// if (!existing) return res.status(404).json({ success: false, message: 'Item not found' });
if (!existing) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Item not found in wishlist',
});
}
await prisma.wishlistItem.delete({
where: { userId_productId: { userId: req.user.id, productId } },
});
// res.json({ success: true, message: 'Item removed from wishlist' });
return res.status(200).json({
statusCode: 200,
status: true,
message: 'Item removed from wishlist',
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,200 @@
const Wardrobe = require('../../models/mongodb/Wardrobe');
// @desc Add item to wardrobe
// exports.addItem = async (req, res, next) => {
// try {
// const itemData = req.body;
// let wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// wardrobe = new Wardrobe({ userId: req.user.id, name: 'My Wardrobe' });
// }
// await wardrobe.addItem(itemData);
// // res.status(201).json({
// // success: true,
// // message: 'Item added successfully',
// // data: { wardrobe },
// // });
// return res.status(201).json({
// statusCode: 201,
// status: true,
// message: 'Item added successfully',
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// };
exports.addItem = async (req, res, next) => {
try {
const itemData = { ...req.body };
// ✅ FIX: map image → images array
if (itemData.image) {
itemData.images = [
{
url: itemData.image,
isPrimary: true,
}
];
delete itemData.image; // prevent schema pollution
}
let wardrobe = await Wardrobe.findByUserId(req.user.id);
if (!wardrobe) {
wardrobe = new Wardrobe({
userId: req.user.id,
name: 'My Wardrobe',
});
}
await wardrobe.addItem(itemData);
return res.status(201).json({
statusCode: 201,
status: true,
message: 'Item added successfully',
data: { wardrobe },
});
} catch (error) {
next(error);
}
};
// @desc Update wardrobe item
exports.updateItem = async (req, res, next) => {
try {
const { itemId } = req.params;
const updateData = req.body;
const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({ success: false, message: 'Wardrobe not found' });
// }
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Wardrobe not found',
});
}
await wardrobe.updateItem(itemId, updateData);
// res.json({ success: true, message: 'Item updated successfully', data: { wardrobe } });
return res.json({
statusCode: 200,
status: true,
message: 'Item updated successfully',
data: { wardrobe },
});
} catch (error) {
next(error);
}
};
// @desc Remove item from wardrobe
exports.removeItem = async (req, res, next) => {
try {
const { itemId } = req.params;
const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({ success: false, message: 'Wardrobe not found' });
// }
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: 'Wardrobe not found',
});
}
await wardrobe.removeItem(itemId);
// res.json({ success: true, message: 'Item removed successfully', data: { wardrobe } });
return res.json({
statusCode: 200,
status: true,
message: 'Item removed successfully',
data: { wardrobe },
});
} catch (error) {
next(error);
}
};
// @desc Get items by category
// exports.getItemsByCategory = async (req, res, next) => {
// try {
// const { category } = req.params;
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
// if (!wardrobe) {
// return res.status(404).json({
// statusCode: 404,
// status: false,
// message: 'Wardrobe not found',
// });
// }
// const items = wardrobe.getItemsByCategory(category);
// // res.json({ success: true, data: { items } });
// return res.json({
// statusCode: 200,
// status: true,
// message: 'Items fetched successfully',
// data: { items },
// });
// } catch (error) {
// next(error);
// }
// };
// controllers/wardrobeController.js
exports.getItemsByCategory = async (req, res, next) => {
try {
const { category } = req.params;
const wardrobe = await Wardrobe.findByUserId(req.user.id);
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: "Wardrobe not found",
});
}
const items = wardrobe.getItemsByCategory(category) || [];
return res.json({
statusCode: 200,
status: true,
message: "Items fetched successfully",
data: { items },
});
} catch (error) {
console.error("Error fetching items by category:", error);
return res.status(500).json({
statusCode: 500,
status: false,
message: "Internal server error",
});
}
};

View File

@@ -0,0 +1,66 @@
const Wardrobe = require('../../models/mongodb/Wardrobe');
// @desc Get user's wardrobe
exports.getWardrobe = async (req, res, next) => {
try {
let wardrobe = await Wardrobe.findByUserId(req.user.id);
if (!wardrobe) {
wardrobe = new Wardrobe({
userId: req.user.id,
name: 'My Wardrobe',
});
await wardrobe.save();
}
// res.json({ success: true, data: { wardrobe } });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Wardrobe fetched successfully",
data: { wardrobe },
});
} catch (error) {
next(error);
}
};
// @desc Update wardrobe details
exports.updateWardrobe = async (req, res, next) => {
try {
const { name, description, isPublic, shareSettings } = req.body;
let wardrobe = await Wardrobe.findByUserId(req.user.id);
if (!wardrobe) {
wardrobe = new Wardrobe({
userId: req.user.id,
name: name || 'My Wardrobe',
description,
isPublic: isPublic || false,
shareSettings: shareSettings || { allowViewing: false, allowRecommendations: false },
});
} else {
wardrobe.name = name || wardrobe.name;
wardrobe.description = description;
wardrobe.isPublic = isPublic ?? wardrobe.isPublic;
wardrobe.shareSettings = shareSettings || wardrobe.shareSettings;
}
await wardrobe.save();
// res.json({
// success: true,
// message: 'Wardrobe updated successfully',
// data: { wardrobe },
// });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Wardrobe updated successfully",
data: { wardrobe },
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,48 @@
const Wardrobe = require('../../models/mongodb/Wardrobe');
// @desc Get public wardrobes
exports.getPublicWardrobes = async (req, res, next) => {
try {
const { page = 1, limit = 20 } = req.query;
const skip = (page - 1) * limit;
const wardrobes = await Wardrobe.findPublicWardrobes(parseInt(limit), parseInt(skip));
// res.json({ success: true, data: { wardrobes } });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Public wardrobes fetched successfully",
data: { wardrobes },
});
} catch (error) {
next(error);
}
};
// @desc Get public wardrobe by ID
exports.getPublicWardrobeById = async (req, res, next) => {
try {
const wardrobe = await Wardrobe.findOne({ _id: req.params.id, isPublic: true });
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: "Wardrobe not found",
});
}
// res.json({ success: true, data: { wardrobe } });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Public wardrobe fetched successfully",
data: { wardrobe },
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,29 @@
const Wardrobe = require('../../models/mongodb/Wardrobe');
// @desc Generate outfit recommendations
exports.getRecommendations = async (req, res, next) => {
try {
const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: "Wardrobe not found",
data: null
});
}
const recommendations = wardrobe.generateOutfitRecommendations();
// res.json({ success: true, data: { recommendations } });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Recommendations fetched successfully",
data: { recommendations }
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,52 @@
const Wardrobe = require('../../models/mongodb/Wardrobe');
// @desc Search wardrobe items
exports.searchItems = async (req, res, next) => {
try {
const { query, tags, category } = req.query;
const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: "Wardrobe not found",
data: null
});
}
let items = wardrobe.items.filter(item => item.isActive);
if (category) items = items.filter(item => item.category === category);
if (tags) {
const tagArray = tags.split(',');
items = items.filter(item =>
tagArray.some(tag =>
item.aiTags.includes(tag) || item.userTags.includes(tag)
)
);
}
if (query) {
const searchTerm = query.toLowerCase();
items = items.filter(item =>
item.name.toLowerCase().includes(searchTerm) ||
item.brand?.toLowerCase().includes(searchTerm) ||
item.description?.toLowerCase().includes(searchTerm)
);
}
// res.json({ success: true, data: { items } });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Items fetched successfully",
data: { items }
});
} catch (error) {
next(error);
}
};

View File

@@ -0,0 +1,39 @@
const Wardrobe = require('../../models/mongodb/Wardrobe');
// @desc Get wardrobe statistics
exports.getStats = async (req, res, next) => {
try {
const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' });
if (!wardrobe) {
return res.status(404).json({
statusCode: 404,
status: false,
message: "Wardrobe not found",
data: null
});
}
const stats = {
totalItems: wardrobe.totalItems,
categoryCounts: wardrobe.categoryCounts,
recentItems: wardrobe.items
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, 5),
mostWornItems: wardrobe.items
.sort((a, b) => (b.purchaseCount || 0) - (a.purchaseCount || 0))
.slice(0, 5),
};
// res.json({ success: true, data: { stats } });
return res.status(200).json({
statusCode: 200,
status: true,
message: "Wardrobe statistics fetched successfully",
data: { stats }
});
} catch (error) {
next(error);
}
};

153
src/middleware/auth.js Normal file
View File

@@ -0,0 +1,153 @@
const jwt = require('jsonwebtoken');
const { prisma } = require('../config/database');
// Protect routes - require authentication
const protect = async (req, res, next) => {
let token;
// Check for token in header
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
try {
// Get token from header
token = req.headers.authorization.split(' ')[1];
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from database
const user = await prisma.user.findUnique({
where: { id: decoded.id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isActive: true,
isVerified: true,
},
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Not authorized, user not found',
});
}
if (!user.isActive) {
return res.status(401).json({
success: false,
message: 'Not authorized, account is deactivated',
});
}
req.user = user;
next();
} catch (error) {
console.error('Auth middleware error:', error);
return res.status(401).json({
success: false,
message: 'Not authorized, token failed',
});
}
} else {
return res.status(401).json({
success: false,
message: 'Not authorized, no token',
});
}
};
// Grant access to specific roles
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Not authorized to access this route',
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: `User role ${req.user.role} is not authorized to access this route`,
});
}
next();
};
};
// Optional auth - doesn't fail if no token
const optionalAuth = async (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
try {
token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await prisma.user.findUnique({
where: { id: decoded.id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isActive: true,
isVerified: true,
},
});
if (user && user.isActive) {
req.user = user;
}
} catch (error) {
// Ignore token errors for optional auth
console.log('Optional auth token error:', error.message);
}
}
next();
};
// Check if user owns resource
const checkOwnership = (resourceUserIdField = 'userId') => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Not authorized',
});
}
// Admin can access any resource
if (req.user.role === 'ADMIN') {
return next();
}
// Check if user owns the resource
const resourceUserId = req.params[resourceUserIdField] || req.body[resourceUserIdField];
if (resourceUserId && resourceUserId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Not authorized to access this resource',
});
}
next();
};
};
module.exports = {
protect,
authorize,
optionalAuth,
checkOwnership,
};

View File

@@ -0,0 +1,61 @@
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error
console.error(err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { message, statusCode: 404 };
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}
// Prisma errors
if (err.code === 'P2002') {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}
if (err.code === 'P2025') {
const message = 'Record not found';
error = { message, statusCode: 404 };
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
const message = 'Invalid token';
error = { message, statusCode: 401 };
}
if (err.name === 'TokenExpiredError') {
const message = 'Token expired';
error = { message, statusCode: 401 };
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
const notFound = (req, res, next) => {
const error = new Error(`Not Found - ${req.originalUrl}`);
res.status(404);
next(error);
};
module.exports = { errorHandler, notFound };

10
src/middleware/upload.js Normal file
View File

@@ -0,0 +1,10 @@
const multer = require("multer");
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 },
});
module.exports = upload;

View File

@@ -0,0 +1,25 @@
const multerS3 = require("multer-s3");
const multer = require("multer");
const s3 = require("../config/s3");
const uploadProfile = multer({
storage: multerS3({
s3: s3,
bucket: process.env.AWS_S3_BUCKET,
acl: "public-read",
key: (req, file, cb) => {
const userId = req.user.id;
const ext = file.originalname.split(".").pop();
cb(null, `profiles/${userId}-${Date.now()}.${ext}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (!file.mimetype.startsWith("image/")) {
return cb(new Error("Only images are allowed!"));
}
cb(null, true);
},
});
module.exports = { uploadProfile };

View File

@@ -0,0 +1,264 @@
const mongoose = require('mongoose');
const variantSchema = new mongoose.Schema({
size: {
type: String,
required: true,
},
color: {
type: String,
required: true,
},
sku: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
min: 0,
},
compareAtPrice: {
type: Number,
min: 0,
},
inventory: {
quantity: {
type: Number,
default: 0,
min: 0,
},
trackInventory: {
type: Boolean,
default: true,
},
},
images: [String],
isActive: {
type: Boolean,
default: true,
},
});
const productSchema = new mongoose.Schema({
// Basic Information
name: {
type: String,
required: true,
trim: true,
},
slug: {
type: String,
required: true,
unique: true,
lowercase: true,
},
description: {
type: String,
required: true,
},
shortDescription: {
type: String,
maxLength: 500,
},
// Categorization
category: {
type: String,
required: true,
},
subcategory: String,
tags: [String],
brand: String,
// Pricing & Inventory
basePrice: {
type: Number,
required: true,
min: 0,
},
compareAtPrice: {
type: Number,
min: 0,
},
costPrice: {
type: Number,
min: 0,
},
// Variants
variants: [variantSchema],
hasVariants: {
type: Boolean,
default: false,
},
// Media
images: {
primary: String,
gallery: [String],
videos: [String],
},
// SEO
metaTitle: String,
metaDescription: String,
metaKeywords: [String],
// Status & Visibility
status: {
type: String,
enum: ['draft', 'active', 'inactive', 'archived'],
default: 'draft',
},
isFeatured: {
type: Boolean,
default: false,
},
isDigital: {
type: Boolean,
default: false,
},
// Physical Attributes
weight: {
value: Number,
unit: {
type: String,
enum: ['g', 'kg', 'lb', 'oz'],
default: 'g',
},
},
dimensions: {
length: Number,
width: Number,
height: Number,
unit: {
type: String,
enum: ['cm', 'in'],
default: 'cm',
},
},
// Analytics
viewCount: {
type: Number,
default: 0,
},
purchaseCount: {
type: Number,
default: 0,
},
// AI Generated Tags
aiTags: [String],
aiGeneratedDescription: String,
// Timestamps
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
publishedAt: Date,
});
// Indexes for better performance
productSchema.index({ slug: 1 });
productSchema.index({ category: 1, status: 1 });
productSchema.index({ brand: 1 });
productSchema.index({ tags: 1 });
productSchema.index({ 'variants.sku': 1 });
productSchema.index({ status: 1, isFeatured: 1 });
productSchema.index({ createdAt: -1 });
// Text search index
productSchema.index({
name: 'text',
description: 'text',
tags: 'text',
brand: 'text',
});
// Virtual for average rating (if implementing ratings)
productSchema.virtual('averageRating').get(function() {
// This would be calculated from reviews in PostgreSQL
return 0;
});
// Pre-save middleware
productSchema.pre('save', function(next) {
this.updatedAt = new Date();
// Auto-generate slug if not provided
if (!this.slug && this.name) {
this.slug = this.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
// Set published date when status changes to active
if (this.isModified('status') && this.status === 'active' && !this.publishedAt) {
this.publishedAt = new Date();
}
next();
});
// Instance methods
productSchema.methods.incrementViewCount = function() {
this.viewCount += 1;
return this.save();
};
productSchema.methods.incrementPurchaseCount = function(quantity = 1) {
this.purchaseCount += quantity;
return this.save();
};
productSchema.methods.getAvailableVariants = function() {
return this.variants.filter(variant =>
variant.isActive &&
(!variant.inventory.trackInventory || variant.inventory.quantity > 0)
);
};
// Static methods
productSchema.statics.findBySlug = function(slug) {
return this.findOne({ slug, status: 'active' });
};
productSchema.statics.findByCategory = function(category, limit = 20, skip = 0) {
return this.find({ category, status: 'active' })
.limit(limit)
.skip(skip)
.sort({ createdAt: -1 });
};
productSchema.statics.searchProducts = function(query, options = {}) {
const { category, brand, minPrice, maxPrice, limit = 20, skip = 0 } = options;
const searchQuery = {
$text: { $search: query },
status: 'active',
};
if (category) searchQuery.category = category;
if (brand) searchQuery.brand = brand;
if (minPrice || maxPrice) {
searchQuery.basePrice = {};
if (minPrice) searchQuery.basePrice.$gte = minPrice;
if (maxPrice) searchQuery.basePrice.$lte = maxPrice;
}
return this.find(searchQuery)
.limit(limit)
.skip(skip)
.sort({ score: { $meta: 'textScore' } });
};
module.exports = mongoose.model('Product', productSchema);

View File

@@ -0,0 +1,254 @@
const mongoose = require('mongoose');
const wardrobeItemSchema = new mongoose.Schema({
// Basic Information
name: {
type: String,
required: true,
},
description: String,
// Category & Type
category: {
type: String,
required: true,
enum: ['tops', 'bottoms', 'dresses', 'outerwear', 'shoes', 'accessories', 'other'],
},
subcategory: String,
brand: String,
color: String,
// Images
images: [{
url: {
type: String,
required: true,
},
isPrimary: {
type: Boolean,
default: false,
},
uploadedAt: {
type: Date,
default: Date.now,
},
}],
// AI Generated Tags
aiTags: [String],
aiColorPalette: [String],
aiStyleTags: [String],
// User Tags
userTags: [String],
// Physical Attributes
size: String,
material: String,
condition: {
type: String,
enum: ['new', 'like-new', 'good', 'fair', 'poor'],
default: 'good',
},
// Status
isActive: {
type: Boolean,
default: true,
},
// Timestamps
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
const wardrobeSchema = new mongoose.Schema({
userId: {
type: String,
required: true,
ref: 'User', // Reference to PostgreSQL User
},
// Wardrobe Information
name: {
type: String,
default: 'My Wardrobe',
},
description: String,
// Items
items: [wardrobeItemSchema],
// Statistics
totalItems: {
type: Number,
default: 0,
},
categoryCounts: {
tops: { type: Number, default: 0 },
bottoms: { type: Number, default: 0 },
dresses: { type: Number, default: 0 },
outerwear: { type: Number, default: 0 },
shoes: { type: Number, default: 0 },
accessories: { type: Number, default: 0 },
other: { type: Number, default: 0 },
},
// AI Analysis
aiAnalysis: {
lastAnalyzed: Date,
dominantColors: [String],
styleProfile: {
casual: Number,
formal: Number,
trendy: Number,
classic: Number,
bohemian: Number,
minimalist: Number,
},
recommendations: [String],
},
// Privacy Settings
isPublic: {
type: Boolean,
default: false,
},
shareSettings: {
allowViewing: Boolean,
allowRecommendations: Boolean,
},
// Timestamps
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});
// Indexes
wardrobeSchema.index({ userId: 1 });
wardrobeSchema.index({ isPublic: 1 });
wardrobeSchema.index({ 'items.category': 1 });
wardrobeSchema.index({ 'items.aiTags': 1 });
// Text search index
wardrobeSchema.index({
name: 'text',
description: 'text',
'items.name': 'text',
'items.brand': 'text',
'items.userTags': 'text',
});
// Pre-save middleware
wardrobeSchema.pre('save', function(next) {
this.updatedAt = new Date();
// Update statistics
this.totalItems = this.items.length;
// Reset category counts
this.categoryCounts = {
tops: 0,
bottoms: 0,
dresses: 0,
outerwear: 0,
shoes: 0,
accessories: 0,
other: 0,
};
// Count items by category
this.items.forEach(item => {
if (this.categoryCounts[item.category] !== undefined) {
this.categoryCounts[item.category]++;
}
});
next();
});
// Instance methods
wardrobeSchema.methods.addItem = function(itemData) {
this.items.push(itemData);
return this.save();
};
wardrobeSchema.methods.removeItem = function(itemId) {
this.items = this.items.filter(item => item._id.toString() !== itemId);
return this.save();
};
wardrobeSchema.methods.updateItem = function(itemId, updateData) {
const item = this.items.id(itemId);
if (item) {
Object.assign(item, updateData);
item.updatedAt = new Date();
return this.save();
}
throw new Error('Item not found');
};
wardrobeSchema.methods.getItemsByCategory = function(category) {
return this.items.filter(item => item.category === category && item.isActive);
};
wardrobeSchema.methods.getItemsByTags = function(tags) {
return this.items.filter(item =>
item.isActive &&
tags.some(tag =>
item.aiTags.includes(tag) ||
item.userTags.includes(tag)
)
);
};
wardrobeSchema.methods.generateOutfitRecommendations = function() {
// This would integrate with the recommendation service
const tops = this.getItemsByCategory('tops');
const bottoms = this.getItemsByCategory('bottoms');
const shoes = this.getItemsByCategory('shoes');
const accessories = this.getItemsByCategory('accessories');
const recommendations = [];
// Simple outfit combination logic
for (let i = 0; i < Math.min(3, tops.length); i++) {
for (let j = 0; j < Math.min(3, bottoms.length); j++) {
for (let k = 0; k < Math.min(2, shoes.length); k++) {
recommendations.push({
id: `${tops[i]._id}-${bottoms[j]._id}-${shoes[k]._id}`,
items: [tops[i], bottoms[j], shoes[k]],
confidence: Math.random(), // This would come from AI analysis
});
}
}
}
return recommendations.slice(0, 10); // Limit to 10 recommendations
};
// Static methods
wardrobeSchema.statics.findByUserId = function(userId) {
return this.findOne({ userId });
};
wardrobeSchema.statics.findPublicWardrobes = function(limit = 20, skip = 0) {
return this.find({ isPublic: true })
.limit(limit)
.skip(skip)
.sort({ updatedAt: -1 });
};
module.exports = mongoose.model('Wardrobe', wardrobeSchema);

324
src/routes/admin.js Normal file
View File

@@ -0,0 +1,324 @@
// const express = require('express');
// const multer = require('multer');
// const { prisma } = require('../config/database');
// const Product = require('../models/mongodb/Product');
// const { protect, authorize } = require('../middleware/auth');
// const dashboard = require('../controllers/admin/dashboardController');
// const users = require('../controllers/admin/userController');
// const orders = require('../controllers/admin/orderController');
// const products = require('../controllers/admin/productController');
// const categories = require('../controllers/admin/categoryController');
// const coupons = require('../controllers/admin/couponController');
// const router = express.Router();
// // ✅ FIXED: Use multer().any() to accept dynamic field names
// const upload = multer({
// storage: multer.memoryStorage(),
// limits: {
// fileSize: 5 * 1024 * 1024, // 5MB limit per file
// },
// });
// // All routes require admin authentication
// router.use(protect);
// router.use(authorize('ADMIN'));
// // @desc Get dashboard statistics
// // @route GET /api/admin/dashboard
// // @access Private/Admin
// router.get('/dashboard', dashboard.getDashboardStats);
// /**
// * @desc Get coupon statistics
// * @route GET /api/coupons/admin/stats
// * @access Private/Admin
// */
// router.get(
// '/stats',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.getCouponStats
// );
// // @desc Get all users with pagination
// // @route GET /api/admin/users
// // @access Private/Admin
// router.get('/users', users.getAllUsers);
// router.get('/users/:id', users.getUserById);
// // @desc Update user status
// // @route PUT /api/admin/users/:id/status
// // @access Private/Admin
// router.put('/users/:id/status', users.updateUserStatus);
// // @desc Get all orders with filters
// // @route GET /api/admin/orders
// // @access Private/Admin
// router.get('/orders', orders.getAllOrders);
// /**
// * @desc Get status change statistics
// * @route GET /api/admin/orders/stats/status-changes
// * @access Private/Admin
// */
// router.get(
// '/stats/status-changes',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// orders.getStatusChangeStats
// );
// /**
// * @desc Get single order with full history
// * @route GET /api/admin/orders/:orderId
// * @access Private/Admin
// */
// router.get(
// '/:orderId',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// orders.getOrderById
// );
// /**
// * @desc Get order status history
// * @route GET /api/admin/orders/:orderId/history
// * @access Private/Admin
// */
// router.get(
// '/:orderId/history',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// orders.getOrderStatusHistory
// );
// //Order Details Page
// router.get('/orders/:id', orders.getOrderDetails);
// // @desc Get all products
// // @route GET /api/admin/products
// // @access Private/Admin
// router.get('/products', products.getAllProducts);
// // @desc Create new product
// // @route POST /api/admin/products
// // @access Private/Admin
// // Create Product Route
// router.post(
// '/products',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// upload.any(), // ✅ This accepts ANY field names (including dynamic variant fields)
// products.createProduct
// );
// /**
// * @desc Get all coupons
// * @route GET /api/coupons/admin
// * @access Private/Admin
// */
// router.get(
// '/coupons',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.getAllCoupons
// );
// // @desc Update product
// // @route PUT /api/admin/products/:id
// // @access Private/Admin
// router.put('/products/:id', products.updateProduct);
// // @desc Delete product
// // @route DELETE /api/admin/products/:id
// // @access Private/Admin
// router.delete('/products/:id', products.deleteProduct);
// // @desc Get all categories
// // @route GET /api/admin/categories
// // @access Private/Admin
// router.get('/categories', categories.getAllCategories);
// // @desc Create new category
// // @route POST /api/admin/categories
// // @access Private/Admin
// router.post('/categories', categories.createCategory);
// router.put('/categories/:id', categories.updateCategory);
// router.delete('/categories/:id', categories.deleteCategory);
// router.patch('/categories/:id/status', categories.toggleCategoryStatus);
// router.patch('/categories/reorder', categories.reorderCategories);
// router.get('/categories/:id', categories.getCategoryById);
// // Category tree / hierarchy
// router.get('/tree', categories.getCategoryHierarchy);
// // @desc Get all coupons
// // @route GET /api/admin/coupons
// // @access Private/Admin
// // router.get('/coupons', coupons.getAllCoupons);
// // @desc Create new coupon
// // @route POST /api/admin/coupons
// // @access Private/Admin
// // router.post('/coupons', coupons.createCoupon);
// // ==========================================
// // ADMIN ROUTES
// // ==========================================
// /**
// * @desc Get single coupon
// * @route GET /api/coupons/admin/:id
// * @access Private/Admin
// */
// router.get(
// '/coupons/:id',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.getCouponById
// );
// /**
// * @desc Create coupon
// * @route POST /api/coupons/admin
// * @access Private/Admin
// */
// router.post(
// '/coupons',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.createCoupon
// );
// /**
// * @desc Update coupon
// * @route PUT /api/coupons/admin/:id
// * @access Private/Admin
// */
// router.put(
// '/coupons/:id',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.updateCoupon
// );
// /**
// * @desc Delete coupon
// * @route DELETE /api/coupons/admin/:id
// * @access Private/Admin
// */
// router.delete(
// '/coupons/:id',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.deleteCoupon
// );
// /**
// * @desc Toggle coupon status
// * @route PATCH /api/coupons/admin/:id/toggle
// * @access Private/Admin
// */
// router.patch(
// '/coupons/:id/toggle',
// protect,
// authorize('ADMIN', 'SUPER_ADMIN'),
// coupons.toggleCouponStatus
// );
// module.exports = router;
// routes/adminRoutes.js - FIXED VERSION with correct route ordering
const express = require('express');
const multer = require('multer');
const { prisma } = require('../config/database');
const Product = require('../models/mongodb/Product');
const { protect, authorize } = require('../middleware/auth');
const dashboard = require('../controllers/admin/dashboardController');
const users = require('../controllers/admin/userController');
const orders = require('../controllers/admin/orderController');
const products = require('../controllers/admin/productController');
const categories = require('../controllers/admin/categoryController');
const coupons = require('../controllers/admin/couponController');
const router = express.Router();
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024,
},
});
// All routes require admin authentication
router.use(protect);
router.use(authorize('ADMIN'));
// ==========================================
// DASHBOARD
// ==========================================
router.get('/dashboard', dashboard.getDashboardStats);
// ==========================================
// USERS
// ==========================================
router.get('/users', users.getAllUsers);
router.get('/users/:id', users.getUserById);
router.put('/users/:id/status', users.updateUserStatus);
// ==========================================
// ORDERS - SPECIFIC ROUTES FIRST
// ==========================================
router.get('/orders/stats/status-changes', orders.getStatusChangeStats);
router.get('/orders', orders.getAllOrders);
router.get('/orders/:id', orders.getOrderDetails);
router.get('/orders/:orderId/history', orders.getOrderStatusHistory);
// router.put('/orders/:orderId/status', orders.updateOrderStatus);
// ==========================================
// PRODUCTS
// ==========================================
router.get('/products', products.getAllProducts);
router.post('/products', upload.any(), products.createProduct);
router.put('/products/:id', products.updateProduct);
router.delete('/products/:id', products.deleteProduct);
// ==========================================
// CATEGORIES - SPECIFIC ROUTES FIRST
// ==========================================
router.get('/tree', categories.getCategoryHierarchy);
router.patch('/categories/reorder', categories.reorderCategories);
router.get('/categories', categories.getAllCategories);
router.post('/categories', categories.createCategory);
router.get('/categories/:id', categories.getCategoryById);
router.put('/categories/:id', categories.updateCategory);
router.delete('/categories/:id', categories.deleteCategory);
router.patch('/categories/:id/status', categories.toggleCategoryStatus);
// ==========================================
// COUPONS - SPECIFIC ROUTES FIRST
// ==========================================
router.get('/stats', coupons.getCouponStats);
router.get('/coupons', coupons.getAllCoupons);
router.post('/coupons', coupons.createCoupon);
router.get('/coupons/:id', coupons.getCouponById);
router.put('/coupons/:id', coupons.updateCoupon);
router.delete('/coupons/:id', coupons.deleteCoupon);
router.patch('/coupons/:id/toggle', coupons.toggleCouponStatus);
module.exports = router;

286
src/routes/auth.js Normal file
View File

@@ -0,0 +1,286 @@
const express = require('express');
const authService = require('../services/authService');
const { protect } = require('../middleware/auth');
const authController = require('../controllers/authController');
const router = express.Router();
// @desc Register user
// @route POST /api/auth/register
// @access Public
// router.post('/register', async (req, res, next) => {
// try {
// const { email, password, firstName, lastName, username, phone } = req.body;
// // Validation
// if (!email || !password) {
// return res.status(400).json({
// success: false,
// message: 'Email and password are required',
// });
// }
// if (password.length < 6) {
// return res.status(400).json({
// success: false,
// message: 'Password must be at least 6 characters',
// });
// }
// const result = await authService.register({
// email,
// password,
// firstName,
// lastName,
// username,
// phone,
// });
// res.status(201).json({
// success: true,
// message: 'User registered successfully',
// data: result,
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/register', authController.register);
// @desc Login user
// @route POST /api/auth/login
// @access Public
// router.post('/login', async (req, res, next) => {
// try {
// const { email, password } = req.body;
// // Validation
// if (!email || !password) {
// return res.status(400).json({
// success: false,
// message: 'Email and password are required',
// });
// }
// const result = await authService.login(email, password);
// res.json({
// success: true,
// message: 'Login successful',
// data: result,
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/login', authController.login);
// @desc Refresh token
// @route POST /api/auth/refresh
// @access Public
// router.post('/refresh', async (req, res, next) => {
// try {
// const { refreshToken } = req.body;
// if (!refreshToken) {
// return res.status(400).json({
// success: false,
// message: 'Refresh token is required',
// });
// }
// const result = await authService.refreshToken(refreshToken);
// res.json({
// success: true,
// message: 'Token refreshed successfully',
// data: result,
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/refresh', authController.refreshToken);
// @desc Logout user
// @route POST /api/auth/logout
// @access Private
// router.post('/logout', protect, async (req, res, next) => {
// try {
// await authService.logout(req.user.id);
// res.json({
// success: true,
// message: 'Logout successful',
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/logout', protect, authController.logout);
// @desc Change password
// @route PUT /api/auth/change-password
// @access Private
// router.put('/change-password', protect, async (req, res, next) => {
// try {
// const { currentPassword, newPassword } = req.body;
// // Validation
// if (!currentPassword || !newPassword) {
// return res.status(400).json({
// success: false,
// message: 'Current password and new password are required',
// });
// }
// if (newPassword.length < 6) {
// return res.status(400).json({
// success: false,
// message: 'New password must be at least 6 characters',
// });
// }
// await authService.changePassword(req.user.id, currentPassword, newPassword);
// res.json({
// success: true,
// message: 'Password changed successfully',
// });
// } catch (error) {
// next(error);
// }
// });
router.put('/change-password', protect, authController.changePassword);
// @desc Request password reset
// @route POST /api/auth/forgot-password
// @access Public
// router.post('/forgot-password', async (req, res, next) => {
// try {
// const { email } = req.body;
// if (!email) {
// return res.status(400).json({
// success: false,
// message: 'Email is required',
// });
// }
// const result = await authService.requestPasswordReset(email);
// res.json({
// success: true,
// message: result.message,
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/forgot-password', authController.forgotPassword);
// @desc Reset password with token
// @route POST /api/auth/reset-password
// @access Public
// router.post('/reset-password', async (req, res, next) => {
// try {
// const { token, newPassword } = req.body;
// // Validation
// if (!token || !newPassword) {
// return res.status(400).json({
// success: false,
// message: 'Token and new password are required',
// });
// }
// if (newPassword.length < 6) {
// return res.status(400).json({
// success: false,
// message: 'Password must be at least 6 characters',
// });
// }
// await authService.resetPassword(token, newPassword);
// res.json({
// success: true,
// message: 'Password reset successfully',
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/reset-password', authController.resetPassword);
// @desc Send verification email
// @route POST /api/auth/send-verification
// @access Private
// router.post('/send-verification', protect, async (req, res, next) => {
// try {
// await authService.sendVerificationEmail(req.user.id);
// res.json({
// success: true,
// message: 'Verification email sent',
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/send-verification', protect, authController.sendVerification);
// @desc Verify email with token
// @route POST /api/auth/verify-email
// @access Public
// router.post('/verify-email', async (req, res, next) => {
// try {
// const { token } = req.body;
// if (!token) {
// return res.status(400).json({
// success: false,
// message: 'Verification token is required',
// });
// }
// await authService.verifyEmail(token);
// res.json({
// success: true,
// message: 'Email verified successfully',
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/verify-email', authController.verifyEmail);
// @desc Get current user profile
// @route GET /api/auth/me
// @access Private
// router.get('/me', protect, async (req, res, next) => {
// try {
// res.json({
// success: true,
// data: {
// user: req.user,
// },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/me', protect, authController.getMe);
module.exports = router;

View File

@@ -0,0 +1,40 @@
const express = require('express');
const { protect, authorize, optionalAuth } = require('../middleware/auth');
// const adminCouponController = require('../controllers/admin/couponController');
const userCouponController = require('../controllers/couponController');
const router = express.Router();
// ==========================================
// USER ROUTES (Public/Customer)
// ==========================================
/**
* @desc Get available coupons
* @route GET /api/coupons/available
* @access Public
*/
router.get('/available', userCouponController.getAvailableCoupons);
/**
* @desc Validate coupon code
* @route POST /api/coupons/validate
* @access Public
*/
router.post('/validate', optionalAuth, userCouponController.validateCoupon);
/**
* @desc Apply coupon to order
* @route POST /api/coupons/apply
* @access Private
*/
router.post('/apply', protect, userCouponController.applyCouponToOrder);
/**
* @desc Remove coupon from order
* @route POST /api/coupons/remove
* @access Private
*/
router.post('/remove', protect, userCouponController.removeCouponFromOrder);
module.exports = router;

View File

@@ -0,0 +1,35 @@
// routes/deliveryRoutes.js
const express = require('express');
const { protect, authorize } = require('../middleware/auth');
const trackingController = require('../controllers/orderTrackingController');
const router = express.Router();
/**
* @desc Get delivery estimation for pincode
* @route POST /api/delivery/estimate
* @access Public
*/
router.post('/estimate', trackingController.getDeliveryEstimate);
/**
* @desc Get order tracking details
* @route GET /api/orders/:orderId/tracking
* @access Private
*/
router.get('/orders/:orderId/tracking', protect, trackingController.getOrderTracking);
/**
* @desc Update order status (Admin)
* @route PUT /api/admin/orders/:orderId/status
* @access Private/Admin
*/
router.put(
'/admin/:orderId/status',
protect,
authorize('ADMIN', 'SUPER_ADMIN'),
trackingController.updateOrderStatus
);
module.exports = router;

100
src/routes/orders.js Normal file
View File

@@ -0,0 +1,100 @@
const express = require('express');
const { prisma } = require('../config/database');
const { protect, authorize } = require('../middleware/auth');
const orderController = require('../controllers/orderController');
const router = express.Router();
// @desc Create new order
// @route POST /api/orders
// @access Private
router.post('/', protect, orderController.createOrder);
// @desc Get user orders
// @route GET /api/orders
// @access Private
router.get('/', protect, orderController.getUserOrders);
// @desc Get single order
// @route GET /api/orders/:id
// @access Private
router.get('/:id', protect, orderController.getOrderById);
// @desc Update order status (Admin only)
// @route PUT /api/orders/:id/status
// @access Private/Admin
router.put(
'/:id/status',
protect,
authorize('ADMIN'),
orderController.updateOrderStatus
);
// @desc Cancel order
// @route PUT /api/orders/:id/cancel
// @access Private
router.put('/:id/cancel', protect, orderController.cancelOrder);
// @desc Return order
// @route PUT /api/orders/:id/return
// @access Private
router.put('/:id/return', protect, orderController.returnOrder);
// @desc Get all orders (Admin only)
// @route GET /api/orders/admin/all
// @access Private/Admin
router.get(
'/admin/all',
protect,
authorize('ADMIN'),
orderController.getAllOrdersAdmin
);
// Admin approve/reject return
router.put(
'/:id/return/status',
protect,
authorize('ADMIN'),
orderController.updateReturnStatus
);
// Admin: list all return requests
// router.get(
// '/admin/returns',
// protect,
// authorize('ADMIN'),
// orderController.getReturnRequestsAdmin
// );
// Admin: list all return requests
router.get(
'/admin/returns',
protect,
authorize('ADMIN'),
orderController.getAdminReturnRequests
);
// Admin: list all returned products (approved/completed)
router.get(
'/admin/returns/list',
protect,
authorize('ADMIN'),
orderController.getReturnedProducts
);
// Get single return request details
router.get(
'/admin/returns/:id',
protect,
authorize('ADMIN'),
orderController.getReturnRequestById
);
module.exports = router;

View File

@@ -0,0 +1,52 @@
// routes/paymentRoutes.js
const express = require('express');
const paytmController = require('../controllers/payment/paytmController');
const { protect, authorize } = require('../middleware/auth');
const router = express.Router();
// ======================
// PAYTM PAYMENT ROUTES
// ======================
/**
* @desc Initiate Paytm Payment
* @route POST /api/payments/paytm/initiate
* @access Private
*/
router.post('/paytm/initiate', protect, paytmController.initiatePayment);
/**
* @desc Paytm Payment Callback (Called by Paytm after payment)
* @route POST /api/payments/paytm/callback
* @access Public (No auth - Paytm calls this)
*/
router.post('/paytm/callback', paytmController.paymentCallback);
/**
* @desc Check Payment Status
* @route GET /api/payments/paytm/status/:orderId
* @access Private
*/
router.get('/paytm/status/:orderId', protect, paytmController.checkPaymentStatus);
/**
* @desc Get Payment Details
* @route GET /api/payments/paytm/:orderId
* @access Private
*/
router.get('/paytm/:orderId', protect, paytmController.getPaymentDetails);
/**
* @desc Process Refund (Admin only)
* @route POST /api/payments/paytm/refund
* @access Private/Admin
*/
router.post(
'/paytm/refund',
protect,
authorize('ADMIN', 'SUPER_ADMIN'),
paytmController.processRefund
);
module.exports = router;

137
src/routes/products.js Normal file
View File

@@ -0,0 +1,137 @@
const express = require('express');
const Product = require('../models/mongodb/Product');
const { protect, authorize, optionalAuth } = require('../middleware/auth');
const productController = require('../controllers/products/productController');
const recommendationController = require('../controllers/products/recommendation');
const router = express.Router();
/**
* @desc Get personalized recommendations for user
* @route GET /api/products/recommendations/personalized
* @access Private/Public
*/
router.get(
'/recommendations/personalized',
optionalAuth,
recommendationController.getPersonalizedRecommendations
);
// SPECIFIC ROUTES FIRST (before /:slug)
// @desc Get products by category
// @route GET /api/products/category/:category
// @access Public
router.get('/categories', productController.getAllCategories);
router.get('/debug-categories', productController.debugMissingCategories);
router.get('/tree', productController.getUserCategoryHierarchy);
// @desc Get available colors/variants for a category
// @route GET /api/products/category-colors/:categorySlug
// @access Public
router.get(
'/category-colors/:categorySlug',
optionalAuth,
productController.getCategoryColors
);
// @desc Get featured products
// @route GET /api/products/featured
// @access Public
router.get('/featured', optionalAuth, productController.getFeaturedProducts);
// @desc Search products
// @route GET /api/products/search/:query
// @access Public
router.get('/search/:query', optionalAuth, productController.searchProducts);
router.get('/most-loved', productController.getMostLovedProducts);
router.get('/new-arrivals', productController.getNewArrivals);
router.get(
'/category/:categorySlug',
optionalAuth,
productController.getProductsByCategory
);
// @desc Get all products
// @route GET /api/products
// @access Public
router.get('/', optionalAuth, productController.getAllProducts);
// ======================
// PRODUCT-SPECIFIC RECOMMENDATION ROUTES (BEFORE /:slug)
// ======================
/**
* @desc Get recommended products for a specific product
* @route GET /api/products/:slug/recommendations
* @access Public
*/
router.get(
'/:slug/recommendations',
optionalAuth,
recommendationController.getProductRecommendations
);
/**
* @desc Get "Customers also bought" products
* @route GET /api/products/:slug/also-bought
* @access Public
*/
router.get(
'/:slug/also-bought',
optionalAuth,
recommendationController.getAlsoBoughtProducts
);
/**
* @desc Get similar products
* @route GET /api/products/:slug/similar
* @access Public
*/
router.get(
'/:slug/similar',
optionalAuth,
recommendationController.getSimilarProducts
);
// @desc Get single product
// @route GET /api/products/:slug
// @access Public
router.get('/:slug', optionalAuth, productController.getProductBySlug);
// ADMIN ROUTES
// @desc Create new product
// @route POST /api/products
// @access Private/Admin
router.post('/', protect, authorize('ADMIN'), productController.createProduct);
// @desc Update product
// @route PUT /api/products/:id
// @access Private/Admin
router.put(
'/:id',
protect,
authorize('ADMIN'),
productController.updateProduct
);
// @desc Delete product
// @route DELETE /api/products/:id
// @access Private/Admin
router.delete(
'/:id',
protect,
authorize('ADMIN'),
productController.deleteProduct
);
module.exports = router;

23
src/routes/reports.js Normal file
View File

@@ -0,0 +1,23 @@
const express = require("express");
const { protect, authorize } = require("../middleware/auth");
const reports = require("../controllers/admin/reportController");
const router = express.Router();
router.use(protect);
router.use(authorize("ADMIN"));
// Reports Endpoints
router.get("/overview", reports.getOverviewReport);
router.get("/sales", reports.getSalesAnalytics);
router.get("/customers", reports.getCustomerStats);
router.get("/sellers", reports.getSellerStats);
router.get("/orders", reports.getOrderAnalytics);
// router.get("/returns", reports.getReturnAnalytics);
router.get("/inventory", reports.getInventoryStats);
router.get("/financial", reports.getFinancialStats);
// router.get("/payout-history", reports.getPayoutHistory);
// router.get("/activity", reports.getActivityFeed);
module.exports = router;

View File

@@ -0,0 +1,17 @@
const express = require("express");
const multer = require("multer");
const { uploadToS3 } = require("../services/s3Upload.service.js");
const router = express.Router();
const upload = multer({ dest: "uploads/" });
router.post("/upload", upload.single("image"), async (req, res) => {
try {
const imageUrl = await uploadToS3(req.file);
res.json({ success: true, imageUrl });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
module.exports = router;

550
src/routes/users.js Normal file
View File

@@ -0,0 +1,550 @@
const express = require('express');
const { prisma } = require('../config/database');
const { protect, authorize, checkOwnership } = require('../middleware/auth');
const profile = require('../controllers/users/profileController');
const address = require('../controllers/users/addressController');
// const orders = require('../controllers/users/orderController');
const orders = require('../controllers/users/orderController');
const wishlist = require('../controllers/users/wishlistController');
const adminUsers = require('../controllers/users/adminUserController');
const cart = require('../controllers/users/cartController');
// import { uploadProfile } from "../middleware/uploadProfile";
const { uploadProfile } = require('../middleware/uploadProfile');
const router = express.Router();
// @desc Get user profile
// @route GET /api/users/profile
// @access Private
// router.get('/profile', protect, async (req, res, next) => {
// try {
// const user = await prisma.user.findUnique({
// where: { id: req.user.id },
// select: {
// id: true,
// email: true,
// username: true,
// firstName: true,
// lastName: true,
// phone: true,
// avatar: true,
// role: true,
// isVerified: true,
// createdAt: true,
// lastLoginAt: true,
// },
// });
// res.json({
// success: true,
// data: { user },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/profile', protect, profile.getProfile);
// @desc Update user profile
// @route PUT /api/users/profile
// @access Private
// router.put('/profile', protect, async (req, res, next) => {
// try {
// const { firstName, lastName, username, phone } = req.body;
// // Check if username is already taken
// if (username) {
// const existingUser = await prisma.user.findFirst({
// where: {
// username,
// NOT: { id: req.user.id },
// },
// });
// if (existingUser) {
// return res.status(400).json({
// success: false,
// message: 'Username already taken',
// });
// }
// }
// const updatedUser = await prisma.user.update({
// where: { id: req.user.id },
// data: {
// firstName,
// lastName,
// username,
// phone,
// },
// select: {
// id: true,
// email: true,
// username: true,
// firstName: true,
// lastName: true,
// phone: true,
// avatar: true,
// role: true,
// isVerified: true,
// },
// });
// res.json({
// success: true,
// message: 'Profile updated successfully',
// data: { user: updatedUser },
// });
// } catch (error) {
// next(error);
// }
// });
router.put('/profile', protect, profile.updateProfile);
// @desc Upload user avatar
// @route POST /api/users/avatar
// @access Private
// router.post('/avatar', protect, async (req, res, next) => {
// try {
// // This would integrate with file upload middleware
// // For now, just return a placeholder
// res.json({
// success: true,
// message: 'Avatar upload endpoint - to be implemented with file upload middleware',
// });
// } catch (error) {
// next(error);
// }
// });
// router.post('/avatar', protect, profile.uploadAvatar);
router.post("/avatar", protect, uploadProfile.single("avatar"), profile.uploadAvatar);
// @desc Get user addresses
// @route GET /api/users/addresses
// @access Private
// router.get('/addresses', protect, async (req, res, next) => {
// try {
// const addresses = await prisma.address.findMany({
// where: { userId: req.user.id },
// orderBy: { createdAt: 'desc' },
// });
// res.json({
// success: true,
// data: { addresses },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/addresses', protect, address.getAddresses);
// @desc Add user address
// @route POST /api/users/addresses
// @access Private
// router.post('/addresses', protect, async (req, res, next) => {
// try {
// const {
// type,
// isDefault,
// firstName,
// lastName,
// company,
// addressLine1,
// addressLine2,
// city,
// state,
// postalCode,
// country,
// phone,
// } = req.body;
// // If this is set as default, unset other default addresses
// if (isDefault) {
// await prisma.address.updateMany({
// where: { userId: req.user.id },
// data: { isDefault: false },
// });
// }
// const address = await prisma.address.create({
// data: {
// userId: req.user.id,
// type,
// isDefault,
// firstName,
// lastName,
// company,
// addressLine1,
// addressLine2,
// city,
// state,
// postalCode,
// country,
// phone,
// },
// });
// res.status(201).json({
// success: true,
// message: 'Address added successfully',
// data: { address },
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/addresses', protect, address.addAddress);
// @desc Update user address
// @route PUT /api/users/addresses/:id
// @access Private
// router.put('/addresses/:id', protect, async (req, res, next) => {
// try {
// const addressId = req.params.id;
// const {
// type,
// isDefault,
// firstName,
// lastName,
// company,
// addressLine1,
// addressLine2,
// city,
// state,
// postalCode,
// country,
// phone,
// } = req.body;
// // Check if address belongs to user
// const existingAddress = await prisma.address.findFirst({
// where: {
// id: addressId,
// userId: req.user.id,
// },
// });
// if (!existingAddress) {
// return res.status(404).json({
// success: false,
// message: 'Address not found',
// });
// }
// // If this is set as default, unset other default addresses
// if (isDefault) {
// await prisma.address.updateMany({
// where: { userId: req.user.id },
// data: { isDefault: false },
// });
// }
// const updatedAddress = await prisma.address.update({
// where: { id: addressId },
// data: {
// type,
// isDefault,
// firstName,
// lastName,
// company,
// addressLine1,
// addressLine2,
// city,
// state,
// postalCode,
// country,
// phone,
// },
// });
// res.json({
// success: true,
// message: 'Address updated successfully',
// data: { address: updatedAddress },
// });
// } catch (error) {
// next(error);
// }
// });
router.put('/addresses/:id', protect, address.updateAddress);
// @desc Delete user address
// @route DELETE /api/users/addresses/:id
// @access Private
// router.delete('/addresses/:id', protect, async (req, res, next) => {
// try {
// const addressId = req.params.id;
// // Check if address belongs to user
// const existingAddress = await prisma.address.findFirst({
// where: {
// id: addressId,
// userId: req.user.id,
// },
// });
// if (!existingAddress) {
// return res.status(404).json({
// success: false,
// message: 'Address not found',
// });
// }
// await prisma.address.delete({
// where: { id: addressId },
// });
// res.json({
// success: true,
// message: 'Address deleted successfully',
// });
// } catch (error) {
// next(error);
// }
// });
router.delete('/addresses/:id', protect, address.deleteAddress);
// @desc Get user orders
// @route GET /api/users/orders
// @access Private
// router.get('/orders', protect, 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),
// },
// },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/orders', protect, orders.getOrders);
// @desc Get user wishlist
// @route GET /api/users/wishlist
// @access Private
// router.get('/wishlist', protect, async (req, res, next) => {
// try {
// const wishlistItems = await prisma.wishlistItem.findMany({
// where: { userId: req.user.id },
// orderBy: { createdAt: 'desc' },
// });
// res.json({
// success: true,
// data: { wishlist: wishlistItems },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/wishlist', protect, wishlist.getWishlist);
// @desc Add item to wishlist
// @route POST /api/users/wishlist
// @access Private
// router.post('/wishlist', protect, async (req, res, next) => {
// try {
// const { productId } = req.body;
// if (!productId) {
// return res.status(400).json({
// success: false,
// message: 'Product ID is required',
// });
// }
// // Check if item already exists in wishlist
// const existingItem = await prisma.wishlistItem.findUnique({
// where: {
// userId_productId: {
// userId: req.user.id,
// productId,
// },
// },
// });
// if (existingItem) {
// return res.status(400).json({
// success: false,
// message: 'Item already in wishlist',
// });
// }
// const wishlistItem = await prisma.wishlistItem.create({
// data: {
// userId: req.user.id,
// productId,
// },
// });
// res.status(201).json({
// success: true,
// message: 'Item added to wishlist',
// data: { wishlistItem },
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/wishlist', protect, wishlist.addToWishlist);
// @desc Remove item from wishlist
// @route DELETE /api/users/wishlist/:productId
// @access Private
// router.delete('/wishlist/:productId', protect, async (req, res, next) => {
// try {
// const { productId } = req.params;
// const wishlistItem = await prisma.wishlistItem.findUnique({
// where: {
// userId_productId: {
// userId: req.user.id,
// productId,
// },
// },
// });
// if (!wishlistItem) {
// return res.status(404).json({
// success: false,
// message: 'Item not found in wishlist',
// });
// }
// await prisma.wishlistItem.delete({
// where: {
// userId_productId: {
// userId: req.user.id,
// productId,
// },
// },
// });
// res.json({
// success: true,
// message: 'Item removed from wishlist',
// });
// } catch (error) {
// next(error);
// }
// });
router.delete('/wishlist/:productId', protect, wishlist.removeFromWishlist);
// @desc Get all users (Admin only)
// @route GET /api/users
// @access Private/Admin
// router.get('/', protect, authorize('ADMIN'), async (req, res, next) => {
// try {
// const { page = 1, limit = 20, role, search } = req.query;
// const skip = (page - 1) * limit;
// const where = {};
// if (role) where.role = role;
// if (search) {
// where.OR = [
// { email: { contains: search, mode: 'insensitive' } },
// { firstName: { contains: search, mode: 'insensitive' } },
// { lastName: { contains: search, mode: 'insensitive' } },
// { username: { contains: search, mode: 'insensitive' } },
// ];
// }
// const [users, total] = await Promise.all([
// prisma.user.findMany({
// where,
// select: {
// id: true,
// email: true,
// username: true,
// firstName: true,
// lastName: true,
// role: true,
// isVerified: true,
// isActive: true,
// createdAt: true,
// lastLoginAt: true,
// },
// orderBy: { createdAt: 'desc' },
// skip: parseInt(skip),
// take: parseInt(limit),
// }),
// prisma.user.count({ where }),
// ]);
// res.json({
// success: true,
// data: {
// users,
// pagination: {
// page: parseInt(page),
// limit: parseInt(limit),
// total,
// pages: Math.ceil(total / limit),
// },
// },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/', protect, authorize('ADMIN'), adminUsers.getAllUsers);
// Get user's shopping cart
router.get('/my-cart', protect, cart.getCart);
// Add product to cart
router.post('/my-cart/add-item', protect, cart.addToCart);
// Update item quantity in cart
router.put('/my-cart/update-item/:productId', protect, cart.updateQuantity);
// Remove item from cart
router.delete('/my-cart/remove-item/:productId', protect, cart.removeFromCart);
// Clear all items from cart
// router.delete('/my-cart/clear', protect, cart.clearCart);
module.exports = router;

393
src/routes/wardrobe.js Normal file
View File

@@ -0,0 +1,393 @@
const express = require('express');
const Wardrobe = require('../models/mongodb/Wardrobe');
const { protect, authorize } = require('../middleware/auth');
const { getWardrobe, updateWardrobe } = require('../controllers/wardrobe/wardrobeMainController');
const { addItem, updateItem, removeItem, getItemsByCategory } = require('../controllers/wardrobe/wardrobeItemController');
const { searchItems } = require('../controllers/wardrobe/wardrobeSearchController');
const { getRecommendations } = require('../controllers/wardrobe/wardrobeRecommendationController');
const { getStats } = require('../controllers/wardrobe/wardrobeStatsController');
const { getPublicWardrobes, getPublicWardrobeById } = require('../controllers/wardrobe/wardrobePublicController');
const router = express.Router();
// @desc Get user's wardrobe
// @route GET /api/wardrobe
// @access Private
// router.get('/', protect, async (req, res, next) => {
// try {
// let wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// // Create wardrobe if it doesn't exist
// wardrobe = new Wardrobe({
// userId: req.user.id,
// name: 'My Wardrobe',
// });
// await wardrobe.save();
// }
// res.json({
// success: true,
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/', protect, getWardrobe);
// @desc Update wardrobe details
// @route PUT /api/wardrobe
// @access Private
// router.put('/', protect, async (req, res, next) => {
// try {
// const { name, description, isPublic, shareSettings } = req.body;
// let wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// wardrobe = new Wardrobe({
// userId: req.user.id,
// name: name || 'My Wardrobe',
// description,
// isPublic: isPublic || false,
// shareSettings: shareSettings || { allowViewing: false, allowRecommendations: false },
// });
// } else {
// wardrobe.name = name || wardrobe.name;
// wardrobe.description = description;
// wardrobe.isPublic = isPublic !== undefined ? isPublic : wardrobe.isPublic;
// wardrobe.shareSettings = shareSettings || wardrobe.shareSettings;
// }
// await wardrobe.save();
// res.json({
// success: true,
// message: 'Wardrobe updated successfully',
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// });
router.put('/', protect, updateWardrobe);
// @desc Add item to wardrobe
// @route POST /api/wardrobe/items
// @access Private
// router.post('/items', protect, async (req, res, next) => {
// try {
// const {
// name,
// description,
// category,
// subcategory,
// brand,
// color,
// size,
// material,
// condition,
// images,
// userTags,
// } = req.body;
// let wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// wardrobe = new Wardrobe({
// userId: req.user.id,
// name: 'My Wardrobe',
// });
// }
// const itemData = {
// name,
// description,
// category,
// subcategory,
// brand,
// color,
// size,
// material,
// condition,
// images: images || [],
// userTags: userTags || [],
// };
// await wardrobe.addItem(itemData);
// res.status(201).json({
// success: true,
// message: 'Item added to wardrobe successfully',
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// });
router.post('/items', protect, addItem);
// @desc Update wardrobe item
// @route PUT /api/wardrobe/items/:itemId
// @access Private
// router.put('/items/:itemId', protect, async (req, res, next) => {
// try {
// const { itemId } = req.params;
// const updateData = req.body;
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// await wardrobe.updateItem(itemId, updateData);
// res.json({
// success: true,
// message: 'Item updated successfully',
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// });
router.put('/items/:itemId', protect, updateItem);
// @desc Remove item from wardrobe
// @route DELETE /api/wardrobe/items/:itemId
// @access Private
// router.delete('/items/:itemId', protect, async (req, res, next) => {
// try {
// const { itemId } = req.params;
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// await wardrobe.removeItem(itemId);
// res.json({
// success: true,
// message: 'Item removed from wardrobe successfully',
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// });
router.delete('/items/:itemId', protect, removeItem);
// @desc Get items by category
// @route GET /api/wardrobe/items/category/:category
// @access Private
// router.get('/items/category/:category', protect, async (req, res, next) => {
// try {
// const { category } = req.params;
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// const items = wardrobe.getItemsByCategory(category);
// res.json({
// success: true,
// data: { items },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/items/category/:category', protect, getItemsByCategory);
// @desc Search wardrobe items
// @route GET /api/wardrobe/items/search
// @access Private
// router.get('/items/search', protect, async (req, res, next) => {
// try {
// const { query, tags, category } = req.query;
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// let items = wardrobe.items.filter(item => item.isActive);
// // Filter by category
// if (category) {
// items = items.filter(item => item.category === category);
// }
// // Filter by tags
// if (tags) {
// const tagArray = tags.split(',');
// items = items.filter(item =>
// tagArray.some(tag =>
// item.aiTags.includes(tag) || item.userTags.includes(tag)
// )
// );
// }
// // Search by query
// if (query) {
// const searchTerm = query.toLowerCase();
// items = items.filter(item =>
// item.name.toLowerCase().includes(searchTerm) ||
// item.brand?.toLowerCase().includes(searchTerm) ||
// item.description?.toLowerCase().includes(searchTerm)
// );
// }
// res.json({
// success: true,
// data: { items },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/items/search', protect, searchItems);
// @desc Generate outfit recommendations
// @route GET /api/wardrobe/recommendations
// @access Private
// router.get('/recommendations', protect, async (req, res, next) => {
// try {
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// const recommendations = wardrobe.generateOutfitRecommendations();
// res.json({
// success: true,
// data: { recommendations },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/recommendations', protect, getRecommendations);
// @desc Get wardrobe statistics
// @route GET /api/wardrobe/stats
// @access Private
// router.get('/stats', protect, async (req, res, next) => {
// try {
// const wardrobe = await Wardrobe.findByUserId(req.user.id);
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// const stats = {
// totalItems: wardrobe.totalItems,
// categoryCounts: wardrobe.categoryCounts,
// recentItems: wardrobe.items
// .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
// .slice(0, 5),
// mostWornItems: wardrobe.items
// .sort((a, b) => (b.purchaseCount || 0) - (a.purchaseCount || 0))
// .slice(0, 5),
// };
// res.json({
// success: true,
// data: { stats },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/stats', protect, getStats);
// @desc Get public wardrobes
// @route GET /api/wardrobe/public
// @access Public
// router.get('/public', async (req, res, next) => {
// try {
// const { page = 1, limit = 20 } = req.query;
// const skip = (page - 1) * limit;
// const wardrobes = await Wardrobe.findPublicWardrobes(
// parseInt(limit),
// parseInt(skip)
// );
// res.json({
// success: true,
// data: { wardrobes },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/public', getPublicWardrobes);
// @desc Get public wardrobe by ID
// @route GET /api/wardrobe/public/:id
// @access Public
// router.get('/public/:id', async (req, res, next) => {
// try {
// const wardrobe = await Wardrobe.findOne({
// _id: req.params.id,
// isPublic: true,
// });
// if (!wardrobe) {
// return res.status(404).json({
// success: false,
// message: 'Wardrobe not found',
// });
// }
// res.json({
// success: true,
// data: { wardrobe },
// });
// } catch (error) {
// next(error);
// }
// });
router.get('/public/:id', getPublicWardrobeById);
module.exports = router;

View File

@@ -0,0 +1,48 @@
const mongoose = require('mongoose');
const Product = require('../models/mongodb/Product');
require('dotenv').config();
const migrateCategoryIds = async () => {
try {
// Connect to MongoDB
await mongoose.connect(process.env.MONGODB_URI || 'your_mongodb_connection_string');
console.log('Connected to MongoDB');
// Map old category IDs to new ones
const categoryMapping = {
'Clothing': 'cmiu33j770005141mz54xgsqe', // Fashion category
'68c123e87e7f9a9b8b123456': 'cmiu34dfg0009141mn8r1dujd', // Men Clothing
'68c123e87e7f9a9b8b123457': 'cmiu355i2000b141m2o7aqlb2', // Women Clothing
'68c123e87e7f9a9b8b123458': 'cmiu3a7je000l141mwx9boup4', // Western Wear
'68c123e87e7f9a9b8b123459': 'cmiu39ncw000j141mxizow1p2', // Lehengas
'68c123e87e7f9a9b8b123460': 'cmiu384il000f141m6obcit4u', // Sarees
'68c123e87e7f9a9b8b123461': 'cmiu39ncw000j141mxizow1p2', // Lehengas
'68c123e87e7f9a9b8b123462': 'cmiu3cuwy000t141mkt4weuy5', // Ethnic Wear
'68c123e87e7f9a9b8b123463': 'cmiu3a7je000l141mwx9boup4', // Western Wear
'cmh4io0fv0001145t4057y8dw': 'cmiu34dfg0009141mn8r1dujd', // Men Clothing
};
let totalUpdated = 0;
for (const [oldId, newId] of Object.entries(categoryMapping)) {
const result = await Product.updateMany(
{ category: oldId },
{ $set: { category: newId } }
);
totalUpdated += result.modifiedCount;
console.log(`✅ Updated ${result.modifiedCount} products from ${oldId} to ${newId}`);
}
console.log(`\n🎉 Migration complete! Total products updated: ${totalUpdated}`);
// Close connection
await mongoose.connection.close();
console.log('MongoDB connection closed');
process.exit(0);
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
}
};
migrateCategoryIds();

130
src/server.js Normal file
View File

@@ -0,0 +1,130 @@
require('dotenv').config();
// import uploadRoutes from "./routes/upload.routes";
const uploadRoutes = require("./routes/upload.routes");
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const {
initializeDatabases,
closeDatabaseConnections,
} = require('./config/database');
const { errorHandler, notFound } = require('./middleware/errorHandler');
const app = express();
const PORT = process.env.PORT || 3000;
// Security middleware
app.use(
helmet({
contentSecurityPolicy: false, // Disable for API
crossOriginEmbedderPolicy: false,
})
);
// CORS configuration
const corsOptions = {
origin: process.env.CORS_ORIGIN?.split(',') || [
'http://localhost:3000',
'http://localhost:3001',
'http://localhost:5173',
'http://localhost:5174',
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
};
app.use(cors(corsOptions));
// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
// Logging middleware
if (process.env.NODE_ENV !== 'test') {
app.use(morgan('combined'));
}
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV,
});
});
// API Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/wardrobe', require('./routes/wardrobe'));
app.use('/api/delivery', require('./routes/deliveryRoutes'));
app.use('/api/coupons', require('./routes/couponRoutes'));
app.use('/api/admin', require('./routes/admin'));
app.use('/api/admin/reports', require('./routes/reports'));
app.use('/api/payments', require('./routes/paymentRoutes'));
// Upload route
app.use("/api", uploadRoutes);
// Root endpoint
app.get('/', (req, res) => {
res.json({
message: 'Vaishnavi Creation API',
version: '1.0.0',
documentation: '/api/docs',
health: '/health',
});
});
// Error handling middleware (must be last)
app.use(notFound);
app.use(errorHandler);
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
await closeDatabaseConnections();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, shutting down gracefully');
await closeDatabaseConnections();
process.exit(0);
});
// Start server
const startServer = async () => {
try {
// Initialize database connections
await initializeDatabases();
// Start the server
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
console.log(`📚 API Documentation: http://localhost:${PORT}/api/docs`);
console.log(`🏥 Health Check: http://localhost:${PORT}/health`);
console.log(`🌍 Environment: ${process.env.NODE_ENV}`);
});
} catch (error) {
console.error('❌ Failed to start server:', error);
process.exit(1);
}
};
// Only start server if this file is run directly
if (require.main === module) {
startServer();
}
module.exports = app;

348
src/services/authService.js Normal file
View File

@@ -0,0 +1,348 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { prisma } = require('../config/database');
const sendEmail = require('../utils/mailer');
class AuthService {
// Generate JWT token
generateToken(payload) {
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
});
}
// Generate refresh token
generateRefreshToken(payload) {
return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d',
});
}
// Hash password
async hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
// Compare password
async comparePassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
// Register new user
async register(userData) {
const { email, password, firstName, lastName, username, phone } = userData;
// Check if user already exists
const existingUser = await prisma.user.findFirst({
where: {
OR: [{ email }, ...(username ? [{ username }] : [])],
},
});
if (existingUser) {
throw new Error('User with this email or username already exists');
}
// Hash password
const passwordHash = await this.hashPassword(password);
// Create user
const user = await prisma.user.create({
data: {
email,
passwordHash,
firstName,
lastName,
username,
phone,
// role: 'CUSTOMER',
role: userData.role || 'CUSTOMER',
isVerified: false,
isActive: true,
},
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isVerified: true,
createdAt: true,
},
});
// Generate tokens
const token = this.generateToken({ id: user.id });
const refreshToken = this.generateRefreshToken({ id: user.id });
return {
user,
token,
refreshToken,
};
}
// Login user
async login(email, password) {
// Find user by email
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
throw new Error('Invalid credentials');
}
// Check if user is active
if (!user.isActive) {
throw new Error('Account is deactivated');
}
// Verify password
const isPasswordValid = await this.comparePassword(
password,
user.passwordHash
);
if (!isPasswordValid) {
throw new Error('Invalid credentials');
}
// Update last login
await prisma.user.update({
where: { id: user.id },
data: { lastLoginAt: new Date() },
});
// Generate tokens
const token = this.generateToken({ id: user.id });
const refreshToken = this.generateRefreshToken({ id: user.id });
// Return user data (without password)
const userData = {
id: user.id,
email: user.email,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
isVerified: user.isVerified,
avatar: user.avatar,
lastLoginAt: user.lastLoginAt,
};
return {
user: userData,
token,
refreshToken,
};
}
// Refresh token
async refreshToken(refreshToken) {
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Find user
const user = await prisma.user.findUnique({
where: { id: decoded.id },
select: {
id: true,
email: true,
username: true,
firstName: true,
lastName: true,
role: true,
isVerified: true,
isActive: true,
},
});
if (!user || !user.isActive) {
throw new Error('Invalid refresh token');
}
// Generate new tokens
const newToken = this.generateToken({ id: user.id });
const newRefreshToken = this.generateRefreshToken({ id: user.id });
return {
token: newToken,
refreshToken: newRefreshToken,
};
} catch (error) {
throw new Error('Invalid refresh token');
}
}
// Change password
async changePassword(userId, currentPassword, newPassword) {
// Find user
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new Error('User not found');
}
// Verify current password
const isCurrentPasswordValid = await this.comparePassword(
currentPassword,
user.passwordHash
);
if (!isCurrentPasswordValid) {
throw new Error('Current password is incorrect');
}
// Hash new password
const newPasswordHash = await this.hashPassword(newPassword);
// Update password
await prisma.user.update({
where: { id: userId },
data: { passwordHash: newPasswordHash },
});
return { message: 'Password changed successfully' };
}
// Reset password request
async requestPasswordReset(email) {
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
// Don't reveal if user exists or not
return { message: 'If the email exists, a reset link has been sent' };
}
// Generate reset token
const resetToken = jwt.sign(
{ id: user.id, type: 'password_reset' },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Use URL from env
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
// In a real application, you would:
// 1. Store the reset token in database with expiry
// 2. Send email with reset link
// 3. Use a proper email service
// Send email
await sendEmail(user.email, 'Reset Your Password', 'reset-password', {
firstName: user.firstName || '',
resetUrl,
});
// await sendEmail(user.email, 'Reset Your Password', html);
console.log(`Password reset token for ${email}: ${resetToken}`);
return { message: 'If the email exists, a reset link has been sent' };
}
// Reset password with token
async resetPassword(token, newPassword) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (decoded.type !== 'password_reset') {
throw new Error('Invalid reset token');
}
// Find user
const user = await prisma.user.findUnique({
where: { id: decoded.id },
});
if (!user) {
throw new Error('User not found');
}
// Hash new password
const passwordHash = await this.hashPassword(newPassword);
// Update password
await prisma.user.update({
where: { id: user.id },
data: { passwordHash },
});
return { message: 'Password reset successfully' };
} catch (error) {
throw new Error('Invalid or expired reset token');
}
}
// Verify email
async verifyEmail(token) {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (decoded.type !== 'email_verification') {
throw new Error('Invalid verification token');
}
// Update user verification status
await prisma.user.update({
where: { id: decoded.id },
data: { isVerified: true },
});
return { message: 'Email verified successfully' };
} catch (error) {
throw new Error('Invalid or expired verification token');
}
}
// Send verification email
async sendVerificationEmail(userId) {
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new Error('User not found');
}
if (user.isVerified) {
throw new Error('Email already verified');
}
// Generate verification token
const verificationToken = jwt.sign(
{ id: user.id, type: 'email_verification' },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`;
await sendEmail(user.email, 'Verify Your Email', 'verify-email', {
firstName: user.firstName || '',
verificationUrl,
});
// await sendEmail(user.email, 'Verify Your Email', html);
// In a real application, you would send an email here
console.log(`Verification token for ${user.email}: ${verificationToken}`);
return { message: 'Verification email sent' };
}
// Logout (invalidate token)
async logout(userId) {
// In a real application, you might want to:
// 1. Add token to a blacklist
// 2. Store invalidated tokens in Redis
// 3. Implement proper session management
return { message: 'Logged out successfully' };
}
}
module.exports = new AuthService();

View File

@@ -0,0 +1,266 @@
// services/deliveryEstimationService.js
/**
* Delivery Estimation Service
* Calculates estimated delivery dates based on:
* - User's pincode/location
* - Order placement time
* - Shipping method
* - Product availability
*/
// Pincode to delivery days mapping (Indian postal system)
const PINCODE_ZONES = {
// Metro Cities (1-2 days)
METRO: {
pincodes: [
// Delhi NCR
/^110\d{3}$/, /^121\d{3}$/, /^122\d{3}$/, /^201\d{3}$/,
// Mumbai
/^400\d{3}$/, /^401\d{3}$/,
// Bangalore
/^560\d{3}$/,
// Chennai
/^600\d{3}$/,
// Hyderabad
/^500\d{3}$/,
// Kolkata
/^700\d{3}$/,
// Pune
/^411\d{3}$/,
// Ahmedabad
/^380\d{3}$/,
],
deliveryDays: { min: 1, max: 2 },
name: 'Metro Cities',
},
// Tier 1 Cities (2-4 days)
TIER_1: {
pincodes: [
/^141\d{3}$/, // Ludhiana
/^160\d{3}$/, // Chandigarh
/^226\d{3}$/, // Lucknow
/^302\d{3}$/, // Jaipur
/^390\d{3}$/, // Vadodara
/^395\d{3}$/, // Surat
/^422\d{3}$/, // Nashik
/^440\d{3}$/, // Nagpur
/^462\d{3}$/, // Bhopal
/^482\d{3}$/, // Indore
/^492\d{3}$/, // Raipur
/^520\d{3}$/, // Vijayawada
/^530\d{3}$/, // Visakhapatnam
/^570\d{3}$/, // Mysore
/^641\d{3}$/, // Coimbatore
/^682\d{3}$/, // Kochi
/^695\d{3}$/, // Trivandrum
/^751\d{3}$/, // Bhubaneswar
/^781\d{3}$/, // Guwahati
/^800\d{3}$/, // Patna
],
deliveryDays: { min: 2, max: 4 },
name: 'Tier 1 Cities',
},
// Tier 2 Cities (3-5 days)
TIER_2: {
pincodes: [
/^1[0-9]{5}$/, // North India (excluding metros)
/^2[0-9]{5}$/, // West India (excluding metros)
/^3[0-9]{5}$/, // Gujarat/Rajasthan
/^4[0-9]{5}$/, // Maharashtra (excluding metros)
/^5[0-9]{5}$/, // South India (excluding metros)
/^6[0-9]{5}$/, // Tamil Nadu/Kerala (excluding metros)
/^7[0-9]{5}$/, // East India
/^8[0-9]{5}$/, // Bihar/Jharkhand
],
deliveryDays: { min: 3, max: 5 },
name: 'Tier 2 Cities',
},
// Remote Areas (5-7 days)
REMOTE: {
pincodes: [
/^17[0-9]{4}$/, // Himachal Pradesh
/^18[0-9]{4}$/, // J&K
/^19[0-9]{4}$/, // J&K
/^73[0-9]{4}$/, // Arunachal Pradesh
/^79[0-9]{4}$/, // Assam/Meghalaya
/^82[0-9]{4}$/, // Jharkhand (remote)
/^84[0-9]{4}$/, // Bihar (remote)
/^85[0-9]{4}$/, // Orissa (remote)
],
deliveryDays: { min: 5, max: 7 },
name: 'Remote Areas',
},
// Default (4-6 days)
DEFAULT: {
deliveryDays: { min: 4, max: 6 },
name: 'Standard Delivery',
},
};
/**
* Get delivery zone based on pincode
*/
function getDeliveryZone(pincode) {
const cleanPincode = pincode.replace(/\s+/g, '');
// Check Metro
if (PINCODE_ZONES.METRO.pincodes.some(pattern => pattern.test(cleanPincode))) {
return PINCODE_ZONES.METRO;
}
// Check Tier 1
if (PINCODE_ZONES.TIER_1.pincodes.some(pattern => pattern.test(cleanPincode))) {
return PINCODE_ZONES.TIER_1;
}
// Check Tier 2
if (PINCODE_ZONES.TIER_2.pincodes.some(pattern => pattern.test(cleanPincode))) {
return PINCODE_ZONES.TIER_2;
}
// Check Remote
if (PINCODE_ZONES.REMOTE.pincodes.some(pattern => pattern.test(cleanPincode))) {
return PINCODE_ZONES.REMOTE;
}
// Default
return PINCODE_ZONES.DEFAULT;
}
/**
* Calculate estimated delivery date
*/
function calculateDeliveryDate(pincode, orderDate = new Date(), shippingMethod = 'STANDARD') {
const zone = getDeliveryZone(pincode);
let { min, max } = zone.deliveryDays;
// Adjust for shipping method
if (shippingMethod === 'EXPRESS') {
min = Math.max(1, min - 1);
max = Math.max(2, max - 1);
}
// Calculate dates
const minDate = addBusinessDays(orderDate, min);
const maxDate = addBusinessDays(orderDate, max);
return {
zone: zone.name,
estimatedDays: { min, max },
estimatedDelivery: {
min: minDate,
max: maxDate,
formatted: formatDateRange(minDate, maxDate),
},
shippingMethod,
};
}
/**
* Add business days (excluding Sundays)
*/
function addBusinessDays(date, days) {
let currentDate = new Date(date);
let addedDays = 0;
while (addedDays < days) {
currentDate.setDate(currentDate.getDate() + 1);
// Skip Sundays (0 = Sunday)
if (currentDate.getDay() !== 0) {
addedDays++;
}
}
return currentDate;
}
/**
* Format date range for display
*/
function formatDateRange(minDate, maxDate) {
const options = {
weekday: 'short',
month: 'short',
day: 'numeric'
};
const minFormatted = minDate.toLocaleDateString('en-IN', options);
const maxFormatted = maxDate.toLocaleDateString('en-IN', options);
// If same month
if (minDate.getMonth() === maxDate.getMonth()) {
return `${minDate.getDate()}-${maxDate.getDate()} ${minDate.toLocaleDateString('en-IN', { month: 'short' })}`;
}
return `${minFormatted} - ${maxFormatted}`;
}
/**
* Get delivery estimation for checkout
*/
async function getDeliveryEstimation(pincode, shippingMethod = 'STANDARD') {
try {
const estimation = calculateDeliveryDate(pincode, new Date(), shippingMethod);
return {
success: true,
data: {
...estimation,
message: `Estimated delivery by ${estimation.estimatedDelivery.formatted}`,
canDeliver: true,
},
};
} catch (error) {
console.error('Delivery estimation error:', error);
return {
success: false,
message: 'Unable to calculate delivery time',
};
}
}
/**
* Check if pincode is serviceable
*/
function isServiceable(pincode) {
const cleanPincode = pincode.replace(/\s+/g, '');
// Basic validation: Indian pincodes are 6 digits
if (!/^\d{6}$/.test(cleanPincode)) {
return false;
}
// All Indian pincodes are serviceable
// You can add specific non-serviceable pincodes here if needed
const nonServiceablePincodes = [
// Add any non-serviceable pincodes
];
return !nonServiceablePincodes.includes(cleanPincode);
}
/**
* Get delivery speed label
*/
function getDeliverySpeedLabel(days) {
if (days <= 2) return 'Express Delivery';
if (days <= 4) return 'Fast Delivery';
if (days <= 6) return 'Standard Delivery';
return 'Extended Delivery';
}
module.exports = {
calculateDeliveryDate,
getDeliveryEstimation,
getDeliveryZone,
isServiceable,
getDeliverySpeedLabel,
addBusinessDays,
PINCODE_ZONES,
};

View File

@@ -0,0 +1,195 @@
// services/inventoryService.js - COMPLETE INVENTORY SYSTEM
const { prisma } = require('../config/database');
const Product = require('../models/mongodb/Product');
/**
* ✅ Auto-reduce stock when order is DELIVERED
*/
async function reduceStockOnDelivery(orderId) {
try {
console.log('📦 Reducing stock for order:', orderId);
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { items: true },
});
if (!order || order.status !== 'DELIVERED') {
console.log('⚠️ Order not delivered, skipping stock reduction');
return null;
}
const results = [];
for (const item of order.items) {
const product = await Product.findById(item.productId);
if (!product) {
console.log(`❌ Product not found: ${item.productId}`);
continue;
}
const currentStock = product.stock || 0;
const newStock = Math.max(0, currentStock - item.quantity);
// Update stock in MongoDB
await Product.findByIdAndUpdate(item.productId, {
stock: newStock,
updatedAt: new Date(),
});
// Create inventory log
await prisma.inventoryLog.create({
data: {
productId: item.productId,
productName: item.productName || product.name,
type: 'SOLD',
quantityChange: -item.quantity,
previousStock: currentStock,
newStock: newStock,
orderId: orderId,
notes: `Order ${order.orderNumber} delivered`,
},
});
console.log(`${product.name}: ${currentStock}${newStock} (-${item.quantity})`);
results.push({
productId: item.productId,
productName: product.name,
reduced: item.quantity,
previousStock: currentStock,
newStock: newStock,
});
}
return results;
} catch (error) {
console.error('❌ Stock reduction error:', error);
throw error;
}
}
/**
* ✅ Get low stock products
*/
async function getLowStockProducts(threshold = 10) {
try {
const products = await Product.find({
status: 'active',
stock: { $lte: threshold },
})
.select('name slug stock basePrice images')
.sort({ stock: 1 })
.lean();
return products.map(product => ({
_id: product._id.toString(),
name: product.name,
slug: product.slug,
stock: product.stock || 0,
basePrice: product.basePrice,
status: product.stock === 0 ? 'OUT_OF_STOCK' : product.stock <= 5 ? 'CRITICAL' : 'LOW',
displayImage: getProductImage(product),
}));
} catch (error) {
console.error('Error fetching low stock:', error);
return [];
}
}
/**
* ✅ Get inventory stats for dashboard
*/
async function getInventoryStats() {
try {
const [totalProducts, outOfStock, criticalStock, lowStock] = await Promise.all([
Product.countDocuments({ status: 'active' }),
Product.countDocuments({ status: 'active', stock: 0 }),
Product.countDocuments({ status: 'active', stock: { $gte: 1, $lte: 5 } }),
Product.countDocuments({ status: 'active', stock: { $gte: 6, $lte: 10 } }),
]);
return {
totalProducts,
outOfStock,
criticalStock,
lowStock,
inStock: totalProducts - outOfStock - criticalStock - lowStock,
};
} catch (error) {
console.error('Error fetching inventory stats:', error);
return null;
}
}
/**
* ✅ Manual stock adjustment (Admin)
*/
async function adjustStock(productId, quantity, type, notes, adminId) {
try {
const product = await Product.findById(productId);
if (!product) throw new Error('Product not found');
const currentStock = product.stock || 0;
let newStock;
switch (type) {
case 'ADD':
newStock = currentStock + quantity;
break;
case 'REMOVE':
newStock = Math.max(0, currentStock - quantity);
break;
case 'SET':
newStock = quantity;
break;
default:
throw new Error('Invalid type');
}
await Product.findByIdAndUpdate(productId, {
stock: newStock,
updatedAt: new Date(),
});
await prisma.inventoryLog.create({
data: {
productId,
productName: product.name,
type: type === 'ADD' ? 'RESTOCK' : 'ADJUSTMENT',
quantityChange: type === 'ADD' ? quantity : -quantity,
previousStock: currentStock,
newStock: newStock,
notes: notes || `Manual ${type} by admin`,
adjustedBy: adminId,
},
});
return {
success: true,
productName: product.name,
previousStock: currentStock,
newStock: newStock,
};
} catch (error) {
console.error('Error adjusting stock:', error);
throw error;
}
}
function getProductImage(product) {
return (
(product.images?.gallery?.[0]) ||
product.images?.primary ||
(product.variants?.[0]?.images?.[0]) ||
'https://via.placeholder.com/300'
);
}
module.exports = {
reduceStockOnDelivery,
getLowStockProducts,
getInventoryStats,
adjustStock,
};

View File

@@ -0,0 +1,25 @@
// services/s3Upload.service.js
import { PutObjectCommand } from "@aws-sdk/client-s3";
import s3 from "../config/s3.js";
import fs from "fs";
export const uploadToS3 = async (file) => {
const fileStream = fs.createReadStream(file.path);
const key = `products/${Date.now()}-${file.originalname}`;
await s3.send(
new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
Body: fileStream,
ContentType: file.mimetype,
ACL: "public-read",
})
);
// optional cleanup
fs.unlinkSync(file.path);
return `https://s3.sahasrarameta.tech/${process.env.AWS_S3_BUCKET}/${key}`;
};

34
src/utils/mailer.js Normal file
View File

@@ -0,0 +1,34 @@
const nodemailer = require('nodemailer');
const ejs = require('ejs');
const path = require('path');
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
const sendEmail = async (to, subject, templateName, templateData) => {
// Render EJS template
const templatePath = path.join(__dirname, '../views/emails', `${templateName}.ejs`);
const html = await ejs.renderFile(templatePath, templateData);
const mailOptions = {
from: `"VC E-Commerce" <${process.env.EMAIL_USER}>`,
to,
subject,
html,
};
try {
await transporter.sendMail(mailOptions);
console.log('Email sent to', to);
} catch (err) {
console.error('Error sending email', err);
throw new Error('Failed to send email');
}
};
module.exports = sendEmail;

315
src/utils/paytm.js Normal file
View File

@@ -0,0 +1,315 @@
// utils/paytm.js
const https = require('https');
const crypto = require('crypto');
/**
* Paytm Configuration
* Add these to your .env file:
* PAYTM_MERCHANT_ID=your_merchant_id
* PAYTM_MERCHANT_KEY=your_merchant_key
* PAYTM_WEBSITE=WEBSTAGING (for staging) or your website name
* PAYTM_CHANNEL_ID=WEB
* PAYTM_INDUSTRY_TYPE=Retail
* PAYTM_HOST=securegw-stage.paytm.in (for staging) or securegw.paytm.in (for production)
*/
const PaytmConfig = {
mid: process.env.PAYTM_MERCHANT_ID,
key: process.env.PAYTM_MERCHANT_KEY,
website: process.env.PAYTM_WEBSITE || 'WEBSTAGING',
channelId: process.env.PAYTM_CHANNEL_ID || 'WEB',
industryType: process.env.PAYTM_INDUSTRY_TYPE || 'Retail',
host: process.env.PAYTM_HOST || 'securegw-stage.paytm.in',
callbackUrl: process.env.PAYTM_CALLBACK_URL || 'http://localhost:3000/api/payments/paytm/callback',
};
console.log(
'Merchant Key Length:',
process.env.PAYTM_MERCHANT_KEY.length
);
/**
* Generate Paytm Checksum
*/
const generateChecksum = (params, merchantKey) => {
return new Promise((resolve, reject) => {
try {
const data = JSON.stringify(params);
const salt = crypto.randomBytes(4).toString('hex');
const hash = crypto
.createHash('sha256')
.update(data + salt)
.digest('hex');
const checksum = hash + salt;
const encryptedChecksum = encrypt(checksum, merchantKey);
resolve(encryptedChecksum);
} catch (err) {
reject(err);
}
});
};
/**
* Verify Paytm Checksum
*/
const verifyChecksum = (params, merchantKey, checksumHash) => {
return new Promise((resolve, reject) => {
try {
const decrypted = decrypt(checksumHash, merchantKey);
const salt = decrypted.slice(-8);
const hash = crypto
.createHash('sha256')
.update(JSON.stringify(params) + salt)
.digest('hex');
resolve(hash + salt === decrypted);
} catch (err) {
reject(err);
}
});
};
/**
* Encrypt data using AES-128-CBC (Paytm standard)
*/
const encrypt = (data, key) => {
if (key.length !== 16) {
throw new Error('Paytm Merchant Key must be exactly 16 characters');
}
const iv = Buffer.from('@@@@&&&&####$$$$'); // Paytm fixed IV
const cipher = crypto.createCipheriv(
'aes-128-cbc',
Buffer.from(key, 'utf8'),
iv
);
let encrypted = cipher.update(data, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
};
/**
* Decrypt data using AES-128-CBC
*/
const decrypt = (encryptedData, key) => {
const iv = Buffer.from('@@@@&&&&####$$$$');
const decipher = crypto.createDecipheriv(
'aes-128-cbc',
Buffer.from(key, 'utf8'),
iv
);
let decrypted = decipher.update(encryptedData, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};
/**
* Initiate Paytm Transaction
*/
const initiateTransaction = async (orderId, amount, customerId, email, mobile) => {
const paytmParams = {
body: {
requestType: 'Payment',
mid: PaytmConfig.mid,
websiteName: PaytmConfig.website,
orderId: orderId,
callbackUrl: PaytmConfig.callbackUrl,
txnAmount: {
value: amount.toString(),
currency: 'INR',
},
userInfo: {
custId: customerId,
email: email,
mobile: mobile,
},
},
};
const checksum = await generateChecksum(
JSON.stringify(paytmParams.body),
PaytmConfig.key
);
paytmParams.head = {
signature: checksum,
};
return new Promise((resolve, reject) => {
const options = {
hostname: PaytmConfig.host,
port: 443,
path: `/theia/api/v1/initiateTransaction?mid=${PaytmConfig.mid}&orderId=${orderId}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
resolve({
success: true,
txnToken: response.body.txnToken,
orderId: orderId,
amount: amount,
...response,
});
} catch (error) {
reject(error);
}
});
});
req.on('error', (error) => {
reject(error);
});
req.write(JSON.stringify(paytmParams));
req.end();
});
};
/**
* Check Transaction Status
*/
const checkTransactionStatus = async (orderId) => {
const paytmParams = {
body: {
mid: PaytmConfig.mid,
orderId: orderId,
},
};
const checksum = await generateChecksum(
JSON.stringify(paytmParams.body),
PaytmConfig.key
);
paytmParams.head = {
signature: checksum,
};
return new Promise((resolve, reject) => {
const options = {
hostname: PaytmConfig.host,
port: 443,
path: `/v3/order/status`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
resolve(response);
} catch (error) {
reject(error);
}
});
});
req.on('error', (error) => {
reject(error);
});
req.write(JSON.stringify(paytmParams));
req.end();
});
};
/**
* Process Refund
*/
const processRefund = async (orderId, refId, txnId, amount) => {
const paytmParams = {
body: {
mid: PaytmConfig.mid,
orderId: orderId,
refId: refId,
txnId: txnId,
txnType: 'REFUND',
refundAmount: amount.toString(),
},
};
const checksum = await generateChecksum(
JSON.stringify(paytmParams.body),
PaytmConfig.key
);
paytmParams.head = {
signature: checksum,
};
return new Promise((resolve, reject) => {
const options = {
hostname: PaytmConfig.host,
port: 443,
path: `/refund/apply`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
resolve(response);
} catch (error) {
reject(error);
}
});
});
req.on('error', (error) => {
reject(error);
});
req.write(JSON.stringify(paytmParams));
req.end();
});
};
module.exports = {
PaytmConfig,
generateChecksum,
verifyChecksum,
initiateTransaction,
checkTransactionStatus,
processRefund,
};

23
src/utils/uploadToS3.js Normal file
View File

@@ -0,0 +1,23 @@
const { PutObjectCommand } = require("@aws-sdk/client-s3");
const s3 = require("../config/s3");
const { v4: uuidv4 } = require("uuid");
const uploadToS3 = async (file, folder = "products") => {
const ext = file.originalname.split(".").pop();
const key = `${folder}/${uuidv4()}.${ext}`;
await s3.send(
new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
})
);
// return `${process.env.AWS_ENDPOINT}/${process.env.AWS_S3_BUCKET}/${key}`;
return `https://${process.env.AWS_ENDPOINT}/${process.env.AWS_S3_BUCKET}/${key}`;
};
module.exports = uploadToS3;

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Password Reset</title>
</head>
<body style="margin:0; padding:0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
<table align="center" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px; margin:20px auto; background-color:#ffffff; border-radius:8px; overflow:hidden; box-shadow:0 4px 8px rgba(0,0,0,0.1);">
<tr>
<td style="background-color:#4CAF50; padding:20px; text-align:center; color:#ffffff; font-size:24px; font-weight:bold;">
VC E-Commerce
</td>
</tr>
<tr>
<td style="padding:30px; text-align:left; color:#333333;">
<h2 style="color:#4CAF50;">Password Reset Request</h2>
<p>Hi <%= firstName %>,</p>
<p>You recently requested to reset your password. Click the button below to reset it. This link is valid for 1 hour only.</p>
<p style="text-align:center; margin:30px 0;">
<a href="<%= resetUrl %>" style="background-color:#4CAF50; color:#ffffff; text-decoration:none; padding:15px 25px; border-radius:5px; display:inline-block; font-weight:bold;">
Reset Password
</a>
</p>
<p>If you did not request a password reset, please ignore this email.</p>
<p>Thanks,<br>The VC E-Commerce Team</p>
</td>
</tr>
<tr>
<td style="background-color:#f4f4f4; padding:20px; text-align:center; color:#777777; font-size:12px;">
&copy; <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved.
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Email Verification</title>
</head>
<body style="margin:0; padding:0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
<table align="center" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px; margin:20px auto; background-color:#ffffff; border-radius:8px; overflow:hidden; box-shadow:0 4px 8px rgba(0,0,0,0.1);">
<tr>
<td style="background-color:#4CAF50; padding:20px; text-align:center; color:#ffffff; font-size:24px; font-weight:bold;">
VC E-Commerce
</td>
</tr>
<tr>
<td style="padding:30px; text-align:left; color:#333333;">
<h2 style="color:#4CAF50;">Email Verification</h2>
<p>Hi <%= firstName %>,</p>
<p>Thank you for registering! Please click the button below to verify your email address and activate your account.</p>
<p style="text-align:center; margin:30px 0;">
<a href="<%= verificationUrl %>" style="background-color:#4CAF50; color:#ffffff; text-decoration:none; padding:15px 25px; border-radius:5px; display:inline-block; font-weight:bold;">
Verify Email
</a>
</p>
<p>If you did not register, you can safely ignore this email.</p>
<p>Thanks,<br>The VC E-Commerce Team</p>
</td>
</tr>
<tr>
<td style="background-color:#f4f4f4; padding:20px; text-align:center; color:#777777; font-size:12px;">
&copy; <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved.
</td>
</tr>
</table>
</body>
</html>