diff --git a/Server/controllers/auth.controller.js b/Server/controllers/auth.controller.js index 08434a1..bc98759 100644 --- a/Server/controllers/auth.controller.js +++ b/Server/controllers/auth.controller.js @@ -1,6 +1,6 @@ require("dotenv").config(); const authService = require("../services/auth.service"); - +const cartService = require("../services/cart.service"); const authController = { login: async (req, res) => { @@ -34,13 +34,20 @@ const authController = { // 1. Extract from the request const { email, password, name, role } = req.body; + try { - // 2. Call the core service function + // 2. Create user first, then create a cart and attach it to avoid orphan carts const newUser = await authService.registerUser(email, password, name, role); - + // create a cart associated with this user + const userCart = await cartService.createCart(newUser.id); + // attach cart to user + const userService = require('../services/user.service'); + await userService.setCartId(newUser.id, userCart._id); + const cartId = await userService.getCartId(newUser.id); + res.status(201).json({ message: 'User registered successfully', - user: newUser + user: { ...newUser, cart: cartId } }); } catch (error) { diff --git a/Server/controllers/cart.controller.js b/Server/controllers/cart.controller.js new file mode 100644 index 0000000..57093bf --- /dev/null +++ b/Server/controllers/cart.controller.js @@ -0,0 +1,97 @@ +const cartService = require('../services/cart.service'); +const userService = require('../services/user.service'); + +class CartController { + + async createCart(req, res) { + try { + // if authenticated, attach cart to user + let customerId = req.user && req.user.id ? req.user.id : null; + const cart = await cartService.createCart(customerId); + // if created for an authenticated user, also persist cart id on user + if (customerId) { + await userService.setCartId(customerId, cart._id); + } + res.status(201).json(cart); + } catch (err) { + res.status(500).json({ error: err.message }); + } + } + + async getCart(req, res) { + try { + let cartId = req.params.cartId; + if (!cartId && req.user && req.user.id) { + cartId = await userService.getCartId(req.user.id); + } + if (!cartId) return res.status(400).json({ error: 'cartId is required' }); + const cart = await cartService.getCart(cartId); + if (!cart) return res.status(404).json({ error: 'Cart not found' }); + res.json(cart); + } catch (err) { + res.status(500).json({ error: err.message }); + } + } + + async addItem(req, res) { + try { + let cartId = req.params.cartId; + if (!cartId && req.user && req.user.id) { + cartId = await userService.getCartId(req.user.id); + } + if (!cartId) return res.status(400).json({ error: 'cartId is required' }); + const item = req.body; + const cart = await cartService.addItem(cartId, item); + res.json(cart); + } catch (err) { + res.status(500).json({ error: err.message }); + } + } + + async removeItem(req, res) { + try { + let cartId = req.params.cartId; + if (!cartId && req.user && req.user.id) { + cartId = await userService.getCartId(req.user.id); + } + if (!cartId) return res.status(400).json({ error: 'cartId is required' }); + const { menuItemId } = req.body; + const cart = await cartService.removeItem(cartId, menuItemId); + res.json(cart); + } catch (err) { + res.status(500).json({ error: err.message }); + } + } + + async updateItemQuantity(req, res) { + try { + let cartId = req.params.cartId; + if (!cartId && req.user && req.user.id) { + cartId = await userService.getCartId(req.user.id); + } + if (!cartId) return res.status(400).json({ error: 'cartId is required' }); + const { menuItemId, quantity } = req.body; + const cart = await cartService.updateItemQuantity(cartId, menuItemId, quantity); + res.json(cart); + } catch (err) { + res.status(500).json({ error: err.message }); + } + } + + async emptyCart(req, res) { + try { + let cartId = req.params.cartId; + if (!cartId && req.user && req.user.id) { + cartId = await userService.getCartId(req.user.id); + } + if (!cartId) return res.status(400).json({ error: 'cartId is required' }); + const cart = await cartService.emptyCart(cartId); + res.json(cart); + } catch (err) { + res.status(500).json({ error: err.message }); + } + } +} + +module.exports = new CartController(); + diff --git a/Server/controllers/order.controller.js b/Server/controllers/order.controller.js new file mode 100644 index 0000000..bc999c8 --- /dev/null +++ b/Server/controllers/order.controller.js @@ -0,0 +1,172 @@ +const orderService = require('../services/order.service'); +const cartService = require('../services/cart.service'); +const userService = require('../services/user.service'); +/** + * Create a new order + * POST /api/orders + */ +async function createOrder(req, res) { + try { + const { items: itemsFromBody, orderType, paymentStatus, customerNotes } = req.body; + const customerId = req.user.id; + + // get customer name from user service + const currentUser = await userService.getCurrentUser(customerId); + const customerName = currentUser.name; + + // if items aren't provided in the request, use the user's cart + let items = itemsFromBody; + let cartId; + if (!items || items.length === 0) { + cartId = await userService.getCartId(customerId); + if (!cartId) { + const error = new Error('No cart found for user'); + error.code = 400; + throw error; + } + const cart = await cartService.getCart(cartId); + if (!cart || !cart.items || cart.items.length === 0) { + const error = new Error('Cart is empty'); + error.code = 400; + throw error; + } + items = cart.items; + } + + const order = await orderService.createOrder({ + customerId, + customerName, + items, + orderType, + paymentStatus, + customerNotes + }); + + // empty the cart if we used it + try { + if (!cartId) cartId = await userService.getCartId(customerId); + if (cartId) await cartService.emptyCart(cartId); + } catch (err) { + console.error('Failed to empty cart after order:', err.message || err); + } + + res.status(201).json({ + success: true, + message: 'Order created successfully', + data: order + }); + } catch (error) { + const statusCode = error.code || 500; + res.status(statusCode).json({ + success: false, + message: error.message + }); + } +} + + + +/** + * Get order by ID + * GET /api/orders/:id + */ +async function getOrderById(req, res) { + try { + const { id } = req.params; + const order = await orderService.getOrderById(id); + + res.status(200).json({ + success: true, + data: order + }); + } catch (error) { + const statusCode = error.code || 500; + res.status(statusCode).json({ + success: false, + message: error.message + }); + } +} + +/** + * Get orders by customer ID + * GET /api/orders/customer/:customerId + */ +async function getOrdersByCustomerId(req, res) { + try { + const { customerId } = req.params; + const orders = await orderService.getOrdersByCustomerId(customerId); + + res.status(200).json({ + success: true, + data: orders + }); + } catch (error) { + const statusCode = error.code || 500; + res.status(statusCode).json({ + success: false, + message: error.message + }); + } +} + +/** + * Get all orders + * GET /api/orders + */ +async function getAllOrders(req, res) { + try { + const orders = await orderService.getAllOrders(); + + res.status(200).json({ + success: true, + data: orders + }); + } catch (error) { + const statusCode = error.code || 500; + res.status(statusCode).json({ + success: false, + message: error.message + }); + } +} + +/** + * Update order status + * PUT /api/orders/:id/status + */ +async function updateOrderStatus(req, res) { + try { + const { id } = req.params; + const { status, paymentStatus } = req.body; + + if (!status && !paymentStatus) { + return res.status(400).json({ + success: false, + message: 'Status or PaymentStatus is required' + }); + } + + const order = await orderService.updateOrderStatus(id, status, paymentStatus); + + res.status(200).json({ + success: true, + message: 'Order status updated successfully', + data: order + }); + } catch (error) { + const statusCode = error.code || 500; + res.status(statusCode).json({ + success: false, + message: error.message + }); + } +} + +module.exports = { + createOrder, + getOrderById, + getOrdersByCustomerId, + getAllOrders, + updateOrderStatus +}; diff --git a/Server/controllers/user.controller.js b/Server/controllers/user.controller.js index f5bc987..3d630bd 100644 --- a/Server/controllers/user.controller.js +++ b/Server/controllers/user.controller.js @@ -60,6 +60,24 @@ const userController = { } }, + getCartId: async (req, res) => { + try { + const { id } = req.user; + const cartId = await userService.getCartId(id); + + res.status(200).json({ + status: 'success', + cartId + }); + } catch (error) { + res.status(error.code || 500).json({ + status: 'error', + message: error.message + }); + } + }, + + updateUserProfile: async (req, res) => { updateUserProfile: async (req, res) => { //can manager also do it? try { const {id} = req.user; diff --git a/Server/models/cart.js b/Server/models/cart.js new file mode 100644 index 0000000..d9aa05e --- /dev/null +++ b/Server/models/cart.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose'); + +const cartSchema = new mongoose.Schema({ + customerId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: false, + index: true + }, + items: { + type: [ + { + menuItemId: { type: mongoose.Schema.Types.ObjectId, ref: 'MenuItem', required: true }, + quantity: { type: Number, required: true, min: 1 } + } + ], + default: [] + }, + totalPrice: { type: Number, default: 0 } +}, { timestamps: true }); + +module.exports = mongoose.model('Cart', cartSchema); diff --git a/Server/models/order.js b/Server/models/order.js new file mode 100644 index 0000000..45f6b94 --- /dev/null +++ b/Server/models/order.js @@ -0,0 +1,54 @@ +const mongoose = require('mongoose'); +// Ensure Cart model is registered before Order (so refs resolve) +require('./cart'); + +const orderSchema = new mongoose.Schema({ + customerId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + + customerName: { + type: String, + required: true, + }, + orderType: { + type : String, + enum: ['Dine-In', 'Takeaway', 'Delivery'], + required: true, + default: 'Dine-In' + }, + status: { + type: String, + enum: ['Pending','In Progress', 'Completed', 'Cancelled'], + required: true, + default: 'In Progress' + }, + totalPrice: { + type: Number, + required: true + }, + paymentStatus: { + type: String, + enum: ['Paid', 'Unpaid', 'Refunded'], + required: true, + default: 'Unpaid' + }, + createdAt: { + type: Date, + required: true, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + }, + customerNotes: { + type: String, + required: false + } +}); + +mongoose.model('Order', orderSchema); +module.exports = mongoose.model('Order', orderSchema); \ No newline at end of file diff --git a/Server/models/user.js b/Server/models/user.js index d293537..c5173ec 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -34,6 +34,12 @@ const userSchema = new mongoose.Schema({ enum: ['Customer', 'Manager', 'Admin'], default: 'Customer', }, + cart: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Cart', + // Make cart optional during initial user creation; it will be attached post-registration + required: false, + }, createdAt: { type: Date, default: Date.now, diff --git a/Server/routes/cart.routes.js b/Server/routes/cart.routes.js new file mode 100644 index 0000000..db95aae --- /dev/null +++ b/Server/routes/cart.routes.js @@ -0,0 +1,30 @@ +const router = require('express').Router(); +const cartController = require('../controllers/cart.controller'); +const authenticationMiddleware = require('../middleware/authentication.middleware'); + +// Create a new cart +router.post('/', authenticationMiddleware, cartController.createCart); + +// Convenience routes for authenticated users (operate on current user's cart) +router.get('/', authenticationMiddleware, cartController.getCart); +router.post('/items', authenticationMiddleware, cartController.addItem); +router.delete('/items', authenticationMiddleware, cartController.removeItem); +router.patch('/items', authenticationMiddleware, cartController.updateItemQuantity); +router.post('/empty', authenticationMiddleware, cartController.emptyCart); + +// Get cart +router.get('/:cartId', cartController.getCart); + +// Add item +router.post('/:cartId/items', cartController.addItem); + +// Remove item +router.delete('/:cartId/items', cartController.removeItem); + +// Update quantity +router.patch('/:cartId/items', cartController.updateItemQuantity); + +// Empty cart +router.post('/:cartId/empty', cartController.emptyCart); + +module.exports = router; diff --git a/Server/routes/order.routes.js b/Server/routes/order.routes.js new file mode 100644 index 0000000..34bed47 --- /dev/null +++ b/Server/routes/order.routes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); + + +const orderController = require('../controllers/order.controller'); +const authMiddleware = require('../middleware/authentication.middleware'); +const authorizationMiddleware = require('../middleware/authorization.middleware'); + +router.post('/', authMiddleware,authorizationMiddleware(['Customer']), orderController.createOrder); +router.get('/', authMiddleware, authorizationMiddleware(['Admin', 'Manager']), orderController.getAllOrders); +router.get('/customer/:customerId', authMiddleware, orderController.getOrdersByCustomerId); +router.get('/:id', authMiddleware, orderController.getOrderById); +router.put('/:id/status', authMiddleware, authorizationMiddleware(['Admin', 'Manager']), orderController.updateOrderStatus); + +module.exports = router; diff --git a/Server/routes/user.routes.js b/Server/routes/user.routes.js index edd1837..ebc283e 100644 --- a/Server/routes/user.routes.js +++ b/Server/routes/user.routes.js @@ -13,6 +13,7 @@ const ROLES = { //user routes router.get("/",authenticationMiddleware,userController.getCurrentUser); +router.get("/cart", authenticationMiddleware, userController.getCartId); router.put("/profile", authenticationMiddleware, userController.updateUserProfile) router.get("/log/:id", authenticationMiddleware, authorizationMiddleware([ROLES.ADMIN]), userController.getLogs) diff --git a/Server/server.js b/Server/server.js index 241e9df..d9a16ad 100644 --- a/Server/server.js +++ b/Server/server.js @@ -29,6 +29,9 @@ const userRouter = require("./routes/user.routes.js") const menuRouter = require("./routes/menu.routes.js") const reservationRouter = require("./routes/reservation.routes.js") const tableRouter = require("./routes/table.routes.js") +const orderRouter = require("./routes/order.routes.js") +const cartRouter = require('./routes/cart.routes.js'); + app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -46,7 +49,8 @@ app.use("/api/v1/auth", authRouter); app.use("/api/v1/reservations", reservationRouter); app.use("/api/v1/tables", tableRouter); app.use("/api/v1/menu", menuRouter); - +app.use("/api/v1/orders", orderRouter); +app.use("/api/v1/cart", cartRouter); // Primary Test Route "http://localhost:PORT/" app.get('/', (req, res) => { res.send('Welcome! Backend started successfully.') diff --git a/Server/services/auth.service.js b/Server/services/auth.service.js index 19a0e05..cd89850 100644 --- a/Server/services/auth.service.js +++ b/Server/services/auth.service.js @@ -103,7 +103,8 @@ const authService = { return { id: newUser._id, name: newUser.name, - email: newUser.email + email: newUser.email, + cart: newUser.cart }; }, }; diff --git a/Server/services/cart.service.js b/Server/services/cart.service.js new file mode 100644 index 0000000..15039bd --- /dev/null +++ b/Server/services/cart.service.js @@ -0,0 +1,96 @@ +const Cart = require('../models/cart.js'); +const MenuItem = require('../models/menu'); + +class CartService { + + // createCart optionally attaches to a user + async createCart(customerId = null) { + const cartData = {}; + if (customerId) cartData.customerId = customerId; + const cart = new Cart(cartData); + return await cart.save(); + } + + async getCart(cartId) { + return await Cart.findById(cartId); + } + + async getCartByUserId(userId) { + return await Cart.findOne({ customerId: userId }); + } + + // compute total using current MenuItem prices + async calculateTotalPrice(cart) { + if (!cart || !cart.items || cart.items.length === 0) return 0; + const menuIds = cart.items.map(i => i.menuItemId); + const menuDocs = await MenuItem.find({ _id: { $in: menuIds } }); + const priceMap = {}; + for (const m of menuDocs) priceMap[m._id.toString()] = m.price || 0; + + return cart.items.reduce((sum, i) => { + const id = i.menuItemId.toString ? i.menuItemId.toString() : String(i.menuItemId); + const price = priceMap[id] ?? 0; + return sum + price * (i.quantity || 0); + }, 0); + } + + async addItem(cartId, item) { + const cart = await this.getCart(cartId); + if (!cart) throw new Error('Cart not found'); + + if (!item || !item.menuItemId) throw new Error('menuItemId is required'); + + const existingItem = cart.items.find(i => i.menuItemId.toString() === item.menuItemId.toString()); + + if (existingItem) { + existingItem.quantity += item.quantity || 1; + } else { + cart.items.push({ menuItemId: item.menuItemId, quantity: item.quantity || 1 }); + } + + cart.totalPrice = await this.calculateTotalPrice(cart); + + return await cart.save(); + } + + async removeItem(cartId, menuItemId) { + const cart = await this.getCart(cartId); + if (!cart) throw new Error('Cart not found'); + + cart.items = cart.items.filter(i => i.menuItemId.toString() !== menuItemId.toString()); + + cart.totalPrice = await this.calculateTotalPrice(cart); + + return await cart.save(); + } + + async updateItemQuantity(cartId, menuItemId, quantity) { + const cart = await this.getCart(cartId); + if (!cart) throw new Error('Cart not found'); + + const item = cart.items.find(i => i.menuItemId.toString() === menuItemId.toString()); + if (!item) throw new Error('Item not found'); + + item.quantity = quantity; + + if (item.quantity <= 0) { + cart.items = cart.items.filter(i => i.menuItemId.toString() !== menuItemId.toString()); + } + + cart.totalPrice = await this.calculateTotalPrice(cart); + + return await cart.save(); + } + + async emptyCart(cartId) { + const cart = await this.getCart(cartId); + if (!cart) throw new Error('Cart not found'); + + cart.items = []; + cart.totalPrice = 0; + + return await cart.save(); + } +} + +module.exports = new CartService(); diff --git a/Server/services/order.service.js b/Server/services/order.service.js new file mode 100644 index 0000000..e5e15e6 --- /dev/null +++ b/Server/services/order.service.js @@ -0,0 +1,97 @@ +const Order = require('../models/order'); +const MenuItem = require('../models/menu'); +const LogModel = require('../models/log'); + +const orderService = { + + async createOrder({ customerId, customerName, items, orderType, paymentStatus, customerNotes }) { + if (!customerId) { + const error = new Error('Customer ID is required'); + error.code = 400; + throw error; + } + + if (!items || !Array.isArray(items) || items.length === 0) { + const error = new Error('Order must contain at least one item'); + error.code = 400; + throw error; + } + + // Calculate total price from current menu item prices + const menuIds = items.map(i => i.menuItemId); + const menuDocs = await MenuItem.find({ _id: { $in: menuIds } }); + const priceMap = {}; + for (const m of menuDocs) priceMap[m._id.toString()] = m.price || 0; + + let totalPrice = 0; + for (const it of items) { + const id = it.menuItemId.toString ? it.menuItemId.toString() : String(it.menuItemId); + const qty = Number(it.quantity) || 1; + const price = priceMap[id] ?? 0; + totalPrice += price * qty; + } + + const order = new Order({ + customerId, + customerName, + orderType: orderType || 'Dine-In', + status: 'Pending', + totalPrice, + paymentStatus: paymentStatus || 'Unpaid', + customerNotes: customerNotes || '' + }); + + await order.save(); + + const log = new LogModel({ + action: 'CREATE', + description: `Order ${order._id} created for user ${customerId}`, + affectedDocument: order._id, + affectedModel: 'Order', + severity: 'NOTICE', + type: 'SUCCESS', + userId: customerId, + }); + await log.save(); + + return order; + }, + + async getOrderById(id) { + const order = await Order.findById(id); + if (!order) { + const error = new Error('Order not found'); + error.code = 404; + throw error; + } + return order; + }, + + async getOrdersByCustomerId(customerId) { + return await Order.find({ customerId }); + }, + + async getAllOrders() { + return await Order.find({}); + }, + + async updateOrderStatus(id, status, paymentStatus) { + const order = await Order.findById(id); + if (!order) { + const error = new Error('Order not found'); + error.code = 404; + throw error; + } + if (status) { + order.status = status; + } + if (paymentStatus) { + order.paymentStatus = paymentStatus; + } + order.updatedAt = Date.now(); + await order.save(); + return order; + } +}; + +module.exports = orderService; \ No newline at end of file diff --git a/Server/services/user.service.js b/Server/services/user.service.js index 93af24d..abe9268 100644 --- a/Server/services/user.service.js +++ b/Server/services/user.service.js @@ -118,7 +118,7 @@ const customerService = { */ async getLogs(id) { if (!id) { - const error = new error('No ID provided'); + const error = new Error('No ID provided'); error.code = 400; throw error; } @@ -131,6 +131,46 @@ const customerService = { throw error; } return logs; + }, + + /** + * Get cart id for a user + * @param {ID} id - The user's ID. + * @returns {ObjectId} The user's cart id + * @throws {Error} If user not found or ID not provided + */ + async getCartId(id) { + if (!id) { + const error = new Error('No ID provided'); + error.code = 400; + throw error; + } + + const user = await UserModel.findById(id); + if (!user) { + const error = new Error('User not found'); + error.code = 404; + throw error; + } + + return user.cart; + }, + + async setCartId(userId, cartId) { + if (!userId) { + const error = new Error('No user ID provided'); + error.code = 400; + throw error; + } + const user = await UserModel.findById(userId); + if (!user) { + const error = new Error('User not found'); + error.code = 404; + throw error; + } + user.cart = cartId; + await user.save(); + return user.cart; } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index d99da01..0000000 --- a/package-lock.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "LayeredDining", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "dotenv": "^17.2.3", - "nodemailer": "^7.0.10" - } - }, - "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/nodemailer": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", - "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 5b76eaf..0000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "dotenv": "^17.2.3", - "nodemailer": "^7.0.10" - } -}