Initial commit
This commit is contained in:
128
server/controllers/authController.js
Normal file
128
server/controllers/authController.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const User = require('../models/User');
|
||||
const { generateToken } = require('../utils/jwt');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
// @desc Register new user
|
||||
// @route POST /api/auth/register
|
||||
// @access Public
|
||||
const registerUser = async (req, res, next) => {
|
||||
try {
|
||||
const { name, email, password, phone } = req.body;
|
||||
|
||||
const userExists = await User.findOne({ email: email.toLowerCase() });
|
||||
if (userExists) {
|
||||
return errorResponse(res, 'User already exists with this email', [], 400);
|
||||
}
|
||||
|
||||
const user = await User.create({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
phone: phone || ''
|
||||
});
|
||||
|
||||
const token = generateToken(user._id, user.role);
|
||||
|
||||
return successResponse(res, 'Registration successful', {
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
token
|
||||
}, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Login user & get token
|
||||
// @route POST /api/auth/login
|
||||
// @access Public
|
||||
const loginUser = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return errorResponse(res, 'Please provide email and password', [], 400);
|
||||
}
|
||||
|
||||
const user = await User.findOne({ email: email.toLowerCase() }).select('+password');
|
||||
if (!user || !(await user.matchPassword(password))) {
|
||||
return errorResponse(res, 'Invalid credentials', [], 401);
|
||||
}
|
||||
|
||||
const token = generateToken(user._id, user.role);
|
||||
|
||||
return successResponse(res, 'Login successful', {
|
||||
_id: user._id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
addresses: user.addresses,
|
||||
token
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get user profile
|
||||
// @route GET /api/auth/profile
|
||||
// @access Private
|
||||
const getUserProfile = async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.user._id);
|
||||
if (!user) {
|
||||
return errorResponse(res, 'User not found', [], 404);
|
||||
}
|
||||
return successResponse(res, 'User profile retrieved', user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Update user profile & addresses
|
||||
// @route PUT /api/auth/profile
|
||||
// @access Private
|
||||
const updateUserProfile = async (req, res, next) => {
|
||||
try {
|
||||
const user = await User.findById(req.user._id);
|
||||
if (!user) {
|
||||
return errorResponse(res, 'User not found', [], 404);
|
||||
}
|
||||
|
||||
user.name = req.body.name || user.name;
|
||||
user.phone = req.body.phone !== undefined ? req.body.phone : user.phone;
|
||||
if (req.body.addresses) {
|
||||
user.addresses = req.body.addresses;
|
||||
}
|
||||
|
||||
if (req.body.password) {
|
||||
user.password = req.body.password;
|
||||
}
|
||||
|
||||
const updatedUser = await user.save();
|
||||
const token = generateToken(updatedUser._id, updatedUser.role);
|
||||
|
||||
return successResponse(res, 'Profile updated successfully', {
|
||||
_id: updatedUser._id,
|
||||
name: updatedUser.name,
|
||||
email: updatedUser.email,
|
||||
role: updatedUser.role,
|
||||
phone: updatedUser.phone,
|
||||
addresses: updatedUser.addresses,
|
||||
token
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
registerUser,
|
||||
loginUser,
|
||||
getUserProfile,
|
||||
updateUserProfile
|
||||
};
|
||||
|
||||
44
server/controllers/bannerController.js
Normal file
44
server/controllers/bannerController.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const Banner = require('../models/Banner');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getBanners = async (req, res, next) => {
|
||||
try {
|
||||
const banners = await Banner.find({ isActive: true }).sort({ displayOrder: 1 });
|
||||
return successResponse(res, 'Hero banners retrieved', banners);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createBanner = async (req, res, next) => {
|
||||
try {
|
||||
const banner = new Banner(req.body);
|
||||
const saved = await banner.save();
|
||||
return successResponse(res, 'Banner created', saved, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateBanner = async (req, res, next) => {
|
||||
try {
|
||||
const banner = await Banner.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
||||
if (!banner) return errorResponse(res, 'Banner not found', [], 404);
|
||||
return successResponse(res, 'Banner updated', banner);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBanner = async (req, res, next) => {
|
||||
try {
|
||||
const banner = await Banner.findByIdAndDelete(req.params.id);
|
||||
if (!banner) return errorResponse(res, 'Banner not found', [], 404);
|
||||
return successResponse(res, 'Banner deleted');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getBanners, createBanner, updateBanner, deleteBanner };
|
||||
|
||||
44
server/controllers/brandController.js
Normal file
44
server/controllers/brandController.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const Brand = require('../models/Brand');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getBrands = async (req, res, next) => {
|
||||
try {
|
||||
const brands = await Brand.find({ isActive: true }).sort({ name: 1 });
|
||||
return successResponse(res, 'Brands retrieved', brands);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createBrand = async (req, res, next) => {
|
||||
try {
|
||||
const brand = new Brand(req.body);
|
||||
const saved = await brand.save();
|
||||
return successResponse(res, 'Brand created', saved, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateBrand = async (req, res, next) => {
|
||||
try {
|
||||
const brand = await Brand.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
||||
if (!brand) return errorResponse(res, 'Brand not found', [], 404);
|
||||
return successResponse(res, 'Brand updated', brand);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBrand = async (req, res, next) => {
|
||||
try {
|
||||
const brand = await Brand.findByIdAndDelete(req.params.id);
|
||||
if (!brand) return errorResponse(res, 'Brand not found', [], 404);
|
||||
return successResponse(res, 'Brand deleted');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getBrands, createBrand, updateBrand, deleteBrand };
|
||||
|
||||
150
server/controllers/cartController.js
Normal file
150
server/controllers/cartController.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const Cart = require('../models/Cart');
|
||||
const Product = require('../models/Product');
|
||||
const Coupon = require('../models/Coupon');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getCart = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user ? req.user._id : null;
|
||||
const sessionId = req.headers['x-session-id'] || null;
|
||||
|
||||
let cart = null;
|
||||
if (userId) {
|
||||
cart = await Cart.findOne({ user: userId }).populate('items.product');
|
||||
} else if (sessionId) {
|
||||
cart = await Cart.findOne({ sessionId }).populate('items.product');
|
||||
}
|
||||
|
||||
if (!cart) {
|
||||
cart = { items: [], couponCode: '', discountAmount: 0 };
|
||||
}
|
||||
|
||||
return successResponse(res, 'Cart retrieved', cart);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const addToCart = async (req, res, next) => {
|
||||
try {
|
||||
const { productId, quantity = 1, variantColor = '', variantFinish = '' } = req.body;
|
||||
const userId = req.user ? req.user._id : null;
|
||||
const sessionId = req.headers['x-session-id'] || 'guest-' + Date.now();
|
||||
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return errorResponse(res, 'Product not found', [], 404);
|
||||
}
|
||||
|
||||
// Inventory check
|
||||
if (product.stock < quantity) {
|
||||
return errorResponse(res, `Only ${product.stock} items remaining in stock`, [], 400);
|
||||
}
|
||||
|
||||
let cart;
|
||||
if (userId) {
|
||||
cart = await Cart.findOne({ user: userId });
|
||||
} else {
|
||||
cart = await Cart.findOne({ sessionId });
|
||||
}
|
||||
|
||||
const price = product.salePrice > 0 ? product.salePrice : product.price;
|
||||
|
||||
if (!cart) {
|
||||
cart = new Cart({
|
||||
user: userId,
|
||||
sessionId,
|
||||
items: [{ product: productId, quantity, variantColor, variantFinish, price }]
|
||||
});
|
||||
} else {
|
||||
const existingItemIndex = cart.items.findIndex(
|
||||
item => item.product.toString() === productId && item.variantColor === variantColor
|
||||
);
|
||||
|
||||
if (existingItemIndex > -1) {
|
||||
cart.items[existingItemIndex].quantity += Number(quantity);
|
||||
} else {
|
||||
cart.items.push({ product: productId, quantity, variantColor, variantFinish, price });
|
||||
}
|
||||
}
|
||||
|
||||
await cart.save();
|
||||
const updatedCart = await Cart.findById(cart._id).populate('items.product');
|
||||
return successResponse(res, 'Product added to cart', updatedCart);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCartItem = async (req, res, next) => {
|
||||
try {
|
||||
const { itemId, quantity } = req.body;
|
||||
const userId = req.user ? req.user._id : null;
|
||||
const sessionId = req.headers['x-session-id'];
|
||||
|
||||
const cart = userId
|
||||
? await Cart.findOne({ user: userId })
|
||||
: await Cart.findOne({ sessionId });
|
||||
|
||||
if (!cart) return errorResponse(res, 'Cart not found', [], 404);
|
||||
|
||||
const item = cart.items.id(itemId);
|
||||
if (!item) return errorResponse(res, 'Item not found in cart', [], 404);
|
||||
|
||||
if (quantity <= 0) {
|
||||
cart.items.pull(itemId);
|
||||
} else {
|
||||
item.quantity = quantity;
|
||||
}
|
||||
|
||||
await cart.save();
|
||||
const updatedCart = await Cart.findById(cart._id).populate('items.product');
|
||||
return successResponse(res, 'Cart updated', updatedCart);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeCartItem = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user ? req.user._id : null;
|
||||
const sessionId = req.headers['x-session-id'];
|
||||
|
||||
const cart = userId
|
||||
? await Cart.findOne({ user: userId })
|
||||
: await Cart.findOne({ sessionId });
|
||||
|
||||
if (cart) {
|
||||
cart.items = cart.items.filter(item => item._id.toString() !== req.params.itemId);
|
||||
await cart.save();
|
||||
}
|
||||
|
||||
const updatedCart = cart ? await Cart.findById(cart._id).populate('items.product') : { items: [] };
|
||||
return successResponse(res, 'Item removed from cart', updatedCart);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const applyCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const { code } = req.body;
|
||||
const coupon = await Coupon.findOne({ code: code.toUpperCase(), isActive: true });
|
||||
|
||||
if (!coupon || coupon.endDate < new Date()) {
|
||||
return errorResponse(res, 'Invalid or expired coupon code', [], 400);
|
||||
}
|
||||
|
||||
return successResponse(res, 'Coupon applied successfully', {
|
||||
code: coupon.code,
|
||||
discountType: coupon.discountType,
|
||||
discountValue: coupon.discountValue,
|
||||
minPurchaseAmount: coupon.minPurchaseAmount
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getCart, addToCart, updateCartItem, removeCartItem, applyCoupon };
|
||||
|
||||
44
server/controllers/categoryController.js
Normal file
44
server/controllers/categoryController.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const Category = require('../models/Category');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getCategories = async (req, res, next) => {
|
||||
try {
|
||||
const categories = await Category.find({ isActive: true }).sort({ displayOrder: 1 });
|
||||
return successResponse(res, 'Categories retrieved', categories);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createCategory = async (req, res, next) => {
|
||||
try {
|
||||
const category = new Category(req.body);
|
||||
const saved = await category.save();
|
||||
return successResponse(res, 'Category created', saved, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCategory = async (req, res, next) => {
|
||||
try {
|
||||
const category = await Category.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
||||
if (!category) return errorResponse(res, 'Category not found', [], 404);
|
||||
return successResponse(res, 'Category updated', category);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCategory = async (req, res, next) => {
|
||||
try {
|
||||
const category = await Category.findByIdAndDelete(req.params.id);
|
||||
if (!category) return errorResponse(res, 'Category not found', [], 404);
|
||||
return successResponse(res, 'Category deleted');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getCategories, createCategory, updateCategory, deleteCategory };
|
||||
|
||||
34
server/controllers/contactController.js
Normal file
34
server/controllers/contactController.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const ContactMessage = require('../models/ContactMessage');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const submitContactForm = async (req, res, next) => {
|
||||
try {
|
||||
const { name, email, phone, subject, message } = req.body;
|
||||
const msg = await ContactMessage.create({ name, email, phone, subject, message });
|
||||
return successResponse(res, 'Thank you! Your message has been received.', msg, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getContactMessages = async (req, res, next) => {
|
||||
try {
|
||||
const messages = await ContactMessage.find({}).sort({ createdAt: -1 });
|
||||
return successResponse(res, 'Contact messages retrieved', messages);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateMessageStatus = async (req, res, next) => {
|
||||
try {
|
||||
const msg = await ContactMessage.findByIdAndUpdate(req.params.id, { status: req.body.status }, { new: true });
|
||||
if (!msg) return errorResponse(res, 'Message not found', [], 404);
|
||||
return successResponse(res, 'Message status updated', msg);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { submitContactForm, getContactMessages, updateMessageStatus };
|
||||
|
||||
34
server/controllers/couponController.js
Normal file
34
server/controllers/couponController.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const Coupon = require('../models/Coupon');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getCoupons = async (req, res, next) => {
|
||||
try {
|
||||
const coupons = await Coupon.find({}).sort({ createdAt: -1 });
|
||||
return successResponse(res, 'Coupons retrieved', coupons);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const coupon = new Coupon(req.body);
|
||||
const saved = await coupon.save();
|
||||
return successResponse(res, 'Coupon created', saved, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCoupon = async (req, res, next) => {
|
||||
try {
|
||||
const coupon = await Coupon.findByIdAndDelete(req.params.id);
|
||||
if (!coupon) return errorResponse(res, 'Coupon not found', [], 404);
|
||||
return successResponse(res, 'Coupon deleted');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getCoupons, createCoupon, deleteCoupon };
|
||||
|
||||
24
server/controllers/emailTemplateController.js
Normal file
24
server/controllers/emailTemplateController.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const EmailTemplate = require('../models/EmailTemplate');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getEmailTemplates = async (req, res, next) => {
|
||||
try {
|
||||
const templates = await EmailTemplate.find({});
|
||||
return successResponse(res, 'Email templates retrieved', templates);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateEmailTemplate = async (req, res, next) => {
|
||||
try {
|
||||
const template = await EmailTemplate.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
||||
if (!template) return errorResponse(res, 'Template not found', [], 404);
|
||||
return successResponse(res, 'Email template updated', template);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getEmailTemplates, updateEmailTemplate };
|
||||
|
||||
85
server/controllers/inventoryController.js
Normal file
85
server/controllers/inventoryController.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const Product = require('../models/Product');
|
||||
const InventoryHistory = require('../models/InventoryHistory');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
// @desc Get stock overview & alerts (Admin)
|
||||
// @route GET /api/inventory/overview
|
||||
// @access Private/Admin
|
||||
const getStockOverview = async (req, res, next) => {
|
||||
try {
|
||||
const products = await Product.find({})
|
||||
.select('name sku stock lowStockThreshold price category brand images')
|
||||
.populate('category', 'name')
|
||||
.populate('brand', 'name');
|
||||
|
||||
const lowStock = products.filter(p => p.stock > 0 && p.stock <= p.lowStockThreshold);
|
||||
const outOfStock = products.filter(p => p.stock === 0);
|
||||
|
||||
return successResponse(res, 'Stock overview retrieved', {
|
||||
totalProducts: products.length,
|
||||
lowStockCount: lowStock.length,
|
||||
outOfStockCount: outOfStock.length,
|
||||
lowStock,
|
||||
outOfStock,
|
||||
products
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Adjust inventory stock level (Admin)
|
||||
// @route POST /api/inventory/adjust
|
||||
// @access Private/Admin
|
||||
const adjustStock = async (req, res, next) => {
|
||||
try {
|
||||
const { productId, newStock, reason, changeType = 'Manual Adjustment' } = req.body;
|
||||
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
return errorResponse(res, 'Product not found', [], 404);
|
||||
}
|
||||
|
||||
const previousStock = product.stock;
|
||||
const quantityChanged = Number(newStock) - previousStock;
|
||||
|
||||
product.stock = Number(newStock);
|
||||
await product.save();
|
||||
|
||||
// Log history
|
||||
const history = await InventoryHistory.create({
|
||||
product: product._id,
|
||||
sku: product.sku,
|
||||
changeType,
|
||||
quantityChanged,
|
||||
previousStock,
|
||||
newStock: product.stock,
|
||||
reason: reason || 'Manual admin inventory adjustment',
|
||||
performedBy: req.user._id
|
||||
});
|
||||
|
||||
return successResponse(res, 'Stock adjusted successfully', { product, history });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get complete inventory change audit log (Admin)
|
||||
// @route GET /api/inventory/history
|
||||
// @access Private/Admin
|
||||
const getInventoryHistory = async (req, res, next) => {
|
||||
try {
|
||||
const history = await InventoryHistory.find({})
|
||||
.populate('product', 'name images')
|
||||
.populate('performedBy', 'name email')
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(100);
|
||||
|
||||
return successResponse(res, 'Inventory history audit log retrieved', history);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getStockOverview, adjustStock, getInventoryHistory };
|
||||
|
||||
212
server/controllers/orderController.js
Normal file
212
server/controllers/orderController.js
Normal file
@@ -0,0 +1,212 @@
|
||||
const Order = require('../models/Order');
|
||||
const Product = require('../models/Product');
|
||||
const InventoryHistory = require('../models/InventoryHistory');
|
||||
const Cart = require('../models/Cart');
|
||||
const { sendEmail } = require('../utils/emailSender');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
// @desc Create new order
|
||||
// @route POST /api/orders
|
||||
// @access Private
|
||||
const createOrder = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
orderItems,
|
||||
shippingAddress,
|
||||
paymentMethod,
|
||||
itemsPrice,
|
||||
taxPrice,
|
||||
shippingPrice,
|
||||
discountAmount,
|
||||
totalAmount
|
||||
} = req.body;
|
||||
|
||||
if (!orderItems || orderItems.length === 0) {
|
||||
return errorResponse(res, 'No order items provided', [], 400);
|
||||
}
|
||||
|
||||
// 1. Check stock and reduce inventory
|
||||
for (const item of orderItems) {
|
||||
const product = await Product.findById(item.product);
|
||||
if (!product) {
|
||||
return errorResponse(res, `Product ${item.name} not found`, [], 400);
|
||||
}
|
||||
if (product.stock < item.quantity) {
|
||||
return errorResponse(res, `Insufficient stock for product ${product.name}`, [], 400);
|
||||
}
|
||||
|
||||
// Record stock deduction
|
||||
const oldStock = product.stock;
|
||||
product.stock -= item.quantity;
|
||||
product.salesCount += item.quantity;
|
||||
await product.save();
|
||||
|
||||
await InventoryHistory.create({
|
||||
product: product._id,
|
||||
sku: product.sku,
|
||||
changeType: 'Order Placed',
|
||||
quantityChanged: -item.quantity,
|
||||
previousStock: oldStock,
|
||||
newStock: product.stock,
|
||||
reason: `Order placed by user ${req.user.name}`,
|
||||
performedBy: req.user._id
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Generate unique order number
|
||||
const orderNumber = `GTR-${Math.floor(100000 + Math.random() * 900000)}`;
|
||||
|
||||
// 3. Save Order
|
||||
const order = new Order({
|
||||
orderNumber,
|
||||
user: req.user._id,
|
||||
orderItems,
|
||||
shippingAddress,
|
||||
paymentMethod,
|
||||
itemsPrice,
|
||||
taxPrice,
|
||||
shippingPrice,
|
||||
discountAmount,
|
||||
totalAmount,
|
||||
isPaid: paymentMethod === 'Stripe', // Auto mark paid for simulated stripe
|
||||
paidAt: paymentMethod === 'Stripe' ? Date.now() : null,
|
||||
orderStatus: paymentMethod === 'Stripe' ? 'Payment Confirmed' : 'Pending',
|
||||
statusHistory: [
|
||||
{
|
||||
status: paymentMethod === 'Stripe' ? 'Payment Confirmed' : 'Pending',
|
||||
note: `Order created via ${paymentMethod}`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const createdOrder = await order.save();
|
||||
|
||||
// 4. Clear user cart
|
||||
await Cart.findOneAndDelete({ user: req.user._id });
|
||||
|
||||
// 5. Send order confirmation email
|
||||
sendEmail({
|
||||
templateName: 'Order Confirmation',
|
||||
to: req.user.email,
|
||||
variables: {
|
||||
customerName: req.user.name,
|
||||
orderId: createdOrder.orderNumber,
|
||||
orderTotal: `$${createdOrder.totalAmount.toFixed(2)}`
|
||||
}
|
||||
});
|
||||
|
||||
return successResponse(res, 'Order created successfully', createdOrder, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get logged in user orders
|
||||
// @route GET /api/orders/my-orders
|
||||
// @access Private
|
||||
const getMyOrders = async (req, res, next) => {
|
||||
try {
|
||||
const orders = await Order.find({ user: req.user._id }).sort({ createdAt: -1 });
|
||||
return successResponse(res, 'User orders retrieved', orders);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get order by ID
|
||||
// @route GET /api/orders/:id
|
||||
// @access Private
|
||||
const getOrderById = async (req, res, next) => {
|
||||
try {
|
||||
const order = await Order.findById(req.params.id).populate('user', 'name email phone');
|
||||
if (!order) {
|
||||
return errorResponse(res, 'Order not found', [], 404);
|
||||
}
|
||||
|
||||
// Check authorization: customer can only view own order unless Admin
|
||||
if (order.user._id.toString() !== req.user._id.toString() && req.user.role !== 'Admin') {
|
||||
return errorResponse(res, 'Not authorized to view this order', [], 403);
|
||||
}
|
||||
|
||||
return successResponse(res, 'Order retrieved', order);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get all orders (Admin)
|
||||
// @route GET /api/orders
|
||||
// @access Private/Admin
|
||||
const getAllOrders = async (req, res, next) => {
|
||||
try {
|
||||
const { status } = req.query;
|
||||
const filter = {};
|
||||
if (status) filter.orderStatus = status;
|
||||
|
||||
const orders = await Order.find(filter)
|
||||
.populate('user', 'name email')
|
||||
.sort({ createdAt: -1 });
|
||||
|
||||
return successResponse(res, 'All orders retrieved', orders);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Update order status & shipping info (Admin)
|
||||
// @route PUT /api/orders/:id/status
|
||||
// @access Private/Admin
|
||||
const updateOrderStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { orderStatus, trackingNumber, shippingProvider, note } = req.body;
|
||||
const order = await Order.findById(req.params.id).populate('user', 'name email');
|
||||
|
||||
if (!order) {
|
||||
return errorResponse(res, 'Order not found', [], 404);
|
||||
}
|
||||
|
||||
if (orderStatus) {
|
||||
order.orderStatus = orderStatus;
|
||||
order.statusHistory.push({
|
||||
status: orderStatus,
|
||||
note: note || `Status updated to ${orderStatus}`
|
||||
});
|
||||
}
|
||||
|
||||
if (trackingNumber) order.trackingNumber = trackingNumber;
|
||||
if (shippingProvider) order.shippingProvider = shippingProvider;
|
||||
|
||||
if (orderStatus === 'Delivered') {
|
||||
order.isDelivered = true;
|
||||
order.deliveredAt = Date.now();
|
||||
}
|
||||
|
||||
const updatedOrder = await order.save();
|
||||
|
||||
// Trigger status change email
|
||||
if (['Shipped', 'Delivered', 'Cancelled'].includes(orderStatus)) {
|
||||
sendEmail({
|
||||
templateName: orderStatus === 'Shipped' ? 'Order Shipped' : 'Order Confirmation',
|
||||
to: order.user.email,
|
||||
variables: {
|
||||
customerName: order.user.name,
|
||||
orderId: order.orderNumber,
|
||||
trackingNumber: order.trackingNumber || 'N/A'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return successResponse(res, 'Order status updated', updatedOrder);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createOrder,
|
||||
getMyOrders,
|
||||
getOrderById,
|
||||
getAllOrders,
|
||||
updateOrderStatus
|
||||
};
|
||||
|
||||
235
server/controllers/productController.js
Normal file
235
server/controllers/productController.js
Normal file
@@ -0,0 +1,235 @@
|
||||
const Product = require('../models/Product');
|
||||
const Category = require('../models/Category');
|
||||
const Brand = require('../models/Brand');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
// @desc Get all products with filtering, search, sorting & pagination
|
||||
// @route GET /api/products
|
||||
// @access Public
|
||||
const getProducts = async (req, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 12;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const query = { isActive: true };
|
||||
|
||||
// Search query
|
||||
if (req.query.search) {
|
||||
const searchRegex = new RegExp(req.query.search, 'i');
|
||||
query.$or = [
|
||||
{ name: searchRegex },
|
||||
{ description: searchRegex },
|
||||
{ tags: searchRegex },
|
||||
{ sku: searchRegex }
|
||||
];
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (req.query.category) {
|
||||
const cat = await Category.findOne({ slug: req.query.category });
|
||||
if (cat) query.category = cat._id;
|
||||
}
|
||||
|
||||
// Brand filter
|
||||
if (req.query.brand) {
|
||||
const brand = await Brand.findOne({ slug: req.query.brand });
|
||||
if (brand) query.brand = brand._id;
|
||||
}
|
||||
|
||||
// Price range filter
|
||||
if (req.query.minPrice || req.query.maxPrice) {
|
||||
query.price = {};
|
||||
if (req.query.minPrice) query.price.$gte = Number(req.query.minPrice);
|
||||
if (req.query.maxPrice) query.price.$lte = Number(req.query.maxPrice);
|
||||
}
|
||||
|
||||
// Guitar specific filters
|
||||
if (req.query.guitarType) {
|
||||
query['specs.guitarType'] = req.query.guitarType;
|
||||
}
|
||||
if (req.query.bodyShape) {
|
||||
query['specs.bodyShape'] = req.query.bodyShape;
|
||||
}
|
||||
if (req.query.handOrientation) {
|
||||
query['specs.handOrientation'] = req.query.handOrientation;
|
||||
}
|
||||
if (req.query.numberOfStrings) {
|
||||
query['specs.numberOfStrings'] = Number(req.query.numberOfStrings);
|
||||
}
|
||||
if (req.query.isFeatured) {
|
||||
query.isFeatured = true;
|
||||
}
|
||||
if (req.query.isNewArrival) {
|
||||
query.isNewArrival = true;
|
||||
}
|
||||
if (req.query.isBestSeller) {
|
||||
query.isBestSeller = true;
|
||||
}
|
||||
|
||||
// Sorting
|
||||
let sortOption = { createdAt: -1 };
|
||||
if (req.query.sort === 'price_asc') sortOption = { price: 1 };
|
||||
if (req.query.sort === 'price_desc') sortOption = { price: -1 };
|
||||
if (req.query.sort === 'rating') sortOption = { rating: -1 };
|
||||
if (req.query.sort === 'bestselling') sortOption = { salesCount: -1 };
|
||||
|
||||
const total = await Product.countDocuments(query);
|
||||
const products = await Product.find(query)
|
||||
.populate('category', 'name slug')
|
||||
.populate('brand', 'name slug logo')
|
||||
.sort(sortOption)
|
||||
.skip(skip)
|
||||
.limit(limit);
|
||||
|
||||
return successResponse(res, 'Products retrieved successfully', {
|
||||
products,
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
pages: Math.ceil(total / limit),
|
||||
limit
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Guitar Finder Recommendation Tool
|
||||
// @route POST /api/products/guitar-finder
|
||||
// @access Public
|
||||
const getGuitarRecommendations = async (req, res, next) => {
|
||||
try {
|
||||
const { skillLevel, acousticElectric, musicStyle, maxBudget, handOrientation } = req.body;
|
||||
|
||||
const query = { isActive: true };
|
||||
|
||||
if (acousticElectric && acousticElectric !== 'Any') {
|
||||
query['specs.guitarType'] = acousticElectric;
|
||||
}
|
||||
|
||||
if (maxBudget) {
|
||||
query.price = { $lte: Number(maxBudget) };
|
||||
}
|
||||
|
||||
if (handOrientation) {
|
||||
query['specs.handOrientation'] = handOrientation;
|
||||
}
|
||||
|
||||
const recommendations = await Product.find(query)
|
||||
.populate('category', 'name slug')
|
||||
.populate('brand', 'name slug logo')
|
||||
.sort({ rating: -1, salesCount: -1 })
|
||||
.limit(6);
|
||||
|
||||
return successResponse(res, 'Guitar recommendations generated', recommendations);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get single product by slug
|
||||
// @route GET /api/products/:slug
|
||||
// @access Public
|
||||
const getProductBySlug = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findOne({ slug: req.params.slug })
|
||||
.populate('category', 'name slug')
|
||||
.populate('brand', 'name slug logo description');
|
||||
|
||||
if (!product) {
|
||||
return errorResponse(res, 'Product not found', [], 404);
|
||||
}
|
||||
|
||||
// Related products (same category)
|
||||
const relatedProducts = await Product.find({
|
||||
category: product.category._id,
|
||||
_id: { $ne: product._id },
|
||||
isActive: true
|
||||
})
|
||||
.limit(4)
|
||||
.select('name slug price salePrice images rating brand');
|
||||
|
||||
return successResponse(res, 'Product retrieved successfully', {
|
||||
product,
|
||||
relatedProducts
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Compare products by array of IDs
|
||||
// @route POST /api/products/compare
|
||||
// @access Public
|
||||
const compareProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { productIds } = req.body;
|
||||
if (!productIds || !Array.isArray(productIds)) {
|
||||
return errorResponse(res, 'Please provide an array of product IDs', [], 400);
|
||||
}
|
||||
|
||||
const products = await Product.find({ _id: { $in: productIds } })
|
||||
.populate('brand', 'name logo')
|
||||
.populate('category', 'name');
|
||||
|
||||
return successResponse(res, 'Comparison data retrieved', products);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Create new product (Admin)
|
||||
// @route POST /api/products
|
||||
// @access Private/Admin
|
||||
const createProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = new Product(req.body);
|
||||
const createdProduct = await product.save();
|
||||
return successResponse(res, 'Product created successfully', createdProduct, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Update product (Admin)
|
||||
// @route PUT /api/products/:id
|
||||
// @access Private/Admin
|
||||
const updateProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
|
||||
if (!product) {
|
||||
return errorResponse(res, 'Product not found', [], 404);
|
||||
}
|
||||
return successResponse(res, 'Product updated successfully', product);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Delete product (Admin)
|
||||
// @route DELETE /api/products/:id
|
||||
// @access Private/Admin
|
||||
const deleteProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndDelete(req.params.id);
|
||||
if (!product) {
|
||||
return errorResponse(res, 'Product not found', [], 404);
|
||||
}
|
||||
return successResponse(res, 'Product deleted successfully');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getProducts,
|
||||
getGuitarRecommendations,
|
||||
getProductBySlug,
|
||||
compareProducts,
|
||||
createProduct,
|
||||
updateProduct,
|
||||
deleteProduct
|
||||
};
|
||||
|
||||
50
server/controllers/reviewController.js
Normal file
50
server/controllers/reviewController.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const Review = require('../models/Review');
|
||||
const Product = require('../models/Product');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getProductReviews = async (req, res, next) => {
|
||||
try {
|
||||
const reviews = await Review.find({ product: req.params.productId, isApproved: true })
|
||||
.populate('user', 'name avatar')
|
||||
.sort({ createdAt: -1 });
|
||||
|
||||
return successResponse(res, 'Reviews retrieved', reviews);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createReview = async (req, res, next) => {
|
||||
try {
|
||||
const { productId, rating, title, comment } = req.body;
|
||||
|
||||
const existing = await Review.findOne({ user: req.user._id, product: productId });
|
||||
if (existing) {
|
||||
return errorResponse(res, 'You have already reviewed this product', [], 400);
|
||||
}
|
||||
|
||||
const review = await Review.create({
|
||||
user: req.user._id,
|
||||
product: productId,
|
||||
rating: Number(rating),
|
||||
title,
|
||||
comment
|
||||
});
|
||||
|
||||
// Update Product average rating
|
||||
const reviews = await Review.find({ product: productId, isApproved: true });
|
||||
const avgRating = reviews.reduce((acc, item) => item.rating + acc, 0) / reviews.length;
|
||||
|
||||
await Product.findByIdAndUpdate(productId, {
|
||||
rating: Number(avgRating.toFixed(1)),
|
||||
numReviews: reviews.length
|
||||
});
|
||||
|
||||
return successResponse(res, 'Review submitted successfully', review, 201);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getProductReviews, createReview };
|
||||
|
||||
32
server/controllers/settingsController.js
Normal file
32
server/controllers/settingsController.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const WebsiteSettings = require('../models/WebsiteSettings');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getSettings = async (req, res, next) => {
|
||||
try {
|
||||
let settings = await WebsiteSettings.findOne({});
|
||||
if (!settings) {
|
||||
settings = await WebsiteSettings.create({});
|
||||
}
|
||||
return successResponse(res, 'Website settings retrieved', settings);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSettings = async (req, res, next) => {
|
||||
try {
|
||||
let settings = await WebsiteSettings.findOne({});
|
||||
if (!settings) {
|
||||
settings = new WebsiteSettings(req.body);
|
||||
} else {
|
||||
Object.assign(settings, req.body);
|
||||
}
|
||||
await settings.save();
|
||||
return successResponse(res, 'Website settings updated successfully', settings);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getSettings, updateSettings };
|
||||
|
||||
33
server/controllers/socialMediaController.js
Normal file
33
server/controllers/socialMediaController.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const SocialMedia = require('../models/SocialMedia');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getSocialMedia = async (req, res, next) => {
|
||||
try {
|
||||
const socialLinks = await SocialMedia.find({ isEnabled: true });
|
||||
return successResponse(res, 'Social media links retrieved', socialLinks);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSocialMedia = async (req, res, next) => {
|
||||
try {
|
||||
const { links } = req.body; // Array of { platform, url, isEnabled }
|
||||
if (Array.isArray(links)) {
|
||||
for (const link of links) {
|
||||
await SocialMedia.findOneAndUpdate(
|
||||
{ platform: link.platform },
|
||||
{ url: link.url, isEnabled: link.isEnabled },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
const updated = await SocialMedia.find({});
|
||||
return successResponse(res, 'Social media accounts updated', updated);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getSocialMedia, updateSocialMedia };
|
||||
|
||||
41
server/controllers/wishlistController.js
Normal file
41
server/controllers/wishlistController.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const Wishlist = require('../models/Wishlist');
|
||||
const { successResponse, errorResponse } = require('../utils/apiResponse');
|
||||
|
||||
const getWishlist = async (req, res, next) => {
|
||||
try {
|
||||
let wishlist = await Wishlist.findOne({ user: req.user._id }).populate('products');
|
||||
if (!wishlist) {
|
||||
wishlist = await Wishlist.create({ user: req.user._id, products: [] });
|
||||
}
|
||||
return successResponse(res, 'Wishlist retrieved', wishlist);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleWishlist = async (req, res, next) => {
|
||||
try {
|
||||
const { productId } = req.body;
|
||||
let wishlist = await Wishlist.findOne({ user: req.user._id });
|
||||
|
||||
if (!wishlist) {
|
||||
wishlist = new Wishlist({ user: req.user._id, products: [productId] });
|
||||
} else {
|
||||
const exists = wishlist.products.includes(productId);
|
||||
if (exists) {
|
||||
wishlist.products.pull(productId);
|
||||
} else {
|
||||
wishlist.products.push(productId);
|
||||
}
|
||||
}
|
||||
|
||||
await wishlist.save();
|
||||
const updated = await Wishlist.findById(wishlist._id).populate('products');
|
||||
return successResponse(res, 'Wishlist updated', updated);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getWishlist, toggleWishlist };
|
||||
|
||||
Reference in New Issue
Block a user