const Order = require('../models/orderModel'); const Product = require('../models/productModel'); const ShippingPrice = require('../models/shippingPriceModel'); const Capital = require('../models/capitalModel'); const Promo = require('../models/promoCodeModel'); const User = require('../models/userModel'); const { sendOrderConfirmationEmail, sendOrderStatusUpdateEmail, sendAdminNewOrderEmail, } = require('../utils/emailService'); const { PERMISSIONS } = require('../utils/permissions'); const { notifyOrderCreated, notifyOrderProcessing, notifyOrderShipped, notifyOrderCompleted, notifyOrderCancelled, notifyVendorNewOrder, notifyProductOutOfStock, notifyAdminsNewOrder, notifyAdminsProductOutOfStock, emitOrderUpdate, } = require('../utils/notificationService'); const { restoreOrderStock, deductOrderStock } = require('../utils/orderUtils'); const { appendOrderToSheet } = require('../utils/googleSheetsService'); exports.getAllOrders = async (req, res) => { try { const orders = await Order.find() .populate('user', 'name email address mobile') .populate({ path: 'items.product', select: 'nameAr nameEn imageCover price provider', populate: { path: 'provider', select: 'storeName name', }, }) .sort({ createdAt: -1 }) .lean(); res.status(200).json({ status: 'success', results: orders.length, data: { orders }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message, }); } }; // Get orders for the authenticated user only exports.getMyOrders = async (req, res) => { try { if (!req.user) { return res.status(401).json({ status: 'fail', message: 'You must be logged in to view your orders', }); } const orders = await Order.find({ user: req.user._id }) .populate('items.product', 'nameAr nameEn imageCover price') .sort({ createdAt: -1 }) .lean(); res.status(200).json({ status: 'success', results: orders.length, data: { orders }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message, }); } }; exports.getOrder = async (req, res) => { try { const order = await Order.findById(req.params.id) .populate({ path: 'items.product', populate: { path: 'provider', model: 'Provider', }, }) .populate('user', 'name email mobile address') .lean(); if (!order) { return res.status(404).json({ status: 'fail', message: 'No order found', }); } // Authorization check let isAuthorized = false; if (req.user) { // 1) Admins and employees with manage_orders permission can see any order const isAdmin = req.user.role === 'admin'; const isEmployee = req.user.role === 'employee' && req.user.permissions.includes(PERMISSIONS.MANAGE_ORDERS); // 2) Users can see their own orders const orderUserId = (order.user && order.user._id) || order.user; const isOwner = orderUserId && String(orderUserId) === String(req.user._id); // 3) Vendors can see the order if it contains one of their products let isVendorOfThisOrder = false; if (req.user.role === 'vendor' && req.user.provider) { const vendorProviderId = String( req.user.provider._id || req.user.provider, ); isVendorOfThisOrder = order.items && order.items.some((item) => { if (!item.product) return false; const itemProviderId = String( (item.product && item.product.provider && item.product.provider._id) || (item.product && item.product.provider) || '', ); return itemProviderId === vendorProviderId; }); } if (isAdmin || isEmployee || isOwner || isVendorOfThisOrder) { isAuthorized = true; } } else { // Guest Access: // If the order has NO user attached, we assume it's a guest order and allow access (since they have the ID) if (!order.user) { isAuthorized = true; } } if (!isAuthorized) { return res.status(403).json({ status: 'fail', message: 'You are not authorized to view this order', }); } res.status(200).json({ status: 'success', data: { order }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message, }); } }; exports.updateOrder = async (req, res) => { try { // Get the old order to check if status changed const oldOrder = await Order.findById(req.params.id); if (!oldOrder) { return res .status(404) .json({ status: 'fail', message: 'No order found' }); } // Prevent moving order away from 'cancelled' if any items are cancelled if (req.body.orderStatus && req.body.orderStatus !== 'cancelled') { const hasCancelledItems = oldOrder.items.some( (item) => item.fulfillmentStatus === 'cancelled', ); if (hasCancelledItems) { // If it's an admin, we allow it BUT we must reset the items and handle stock if (req.user && req.user.role === 'admin') { // Reset all cancelled items to pending const updatedItems = oldOrder.items.map((item) => { if (item.fulfillmentStatus === 'cancelled') { return { ...item.toObject(), fulfillmentStatus: 'pending' }; } return item.toObject(); }); req.body.items = updatedItems; // If moving from 'cancelled' order status, deduct stock again if (oldOrder.orderStatus === 'cancelled') { await deductOrderStock(oldOrder.items); } } else { return res.status(400).json({ status: 'fail', message: 'نعتذر، لا يمكن تغيير حالة الطلب بينما توجد منتجات ملغاة. يجب معالجة المنتجات الملغاة أولاً.', }); } } } // Handle moving TO cancelled status - propagate to all items if ( req.body.orderStatus === 'cancelled' && oldOrder.orderStatus !== 'cancelled' ) { const updatedItems = oldOrder.items.map((item) => ({ ...item.toObject(), fulfillmentStatus: 'cancelled', })); req.body.items = updatedItems; } // Check if status is being changed to shipped or completed and all items are ready if ( req.body.orderStatus && ['shipped', 'completed'].includes(req.body.orderStatus) ) { const allReady = oldOrder.items.every( (item) => item.fulfillmentStatus === 'ready', ); if (!allReady) { return res.status(400).json({ status: 'fail', message: 'Cannot change order status to shipped/completed until all items are marked as ready.', }); } } const updatedOrder = await Order.findByIdAndUpdate( req.params.id, req.body, { new: true, runValidators: true }, ) .populate({ path: 'items.product', populate: { path: 'provider', model: 'Provider', }, }) .populate('user', 'name email'); // Check if order status changed and send email + notification if ( req.body.orderStatus && req.body.orderStatus !== oldOrder.orderStatus && updatedOrder.user ) { // If status changed to cancelled, restore stock if (req.body.orderStatus === 'cancelled') { await restoreOrderStock(updatedOrder.items); // ── Capital: Reverse profit if order was previously completed ── if (oldOrder.orderStatus === 'completed') { try { const populatedForReversal = await Order.findById( updatedOrder._id, ).populate({ path: 'items.product', select: 'costPrice', }); let reversedProfit = 0; for (const item of populatedForReversal.items) { if (!item.product) continue; const costPrice = item.product.costPrice || 0; const sellingPrice = item.unitPrice; reversedProfit += (sellingPrice - costPrice) * item.quantity; } if (reversedProfit > 0) { const capital = await Capital.getCapital(); capital.currentCapital -= reversedProfit; capital.logs.push({ type: 'adjustment', amount: -reversedProfit, balanceAfter: capital.currentCapital, reference: updatedOrder._id, referenceModel: 'Order', description: `Reversed profit — completed order #${updatedOrder._id.toString().slice(-8).toUpperCase()} was cancelled`, date: new Date(), createdBy: req.user._id, }); await capital.save(); } } catch (capitalErr) { console.error( 'Failed to reverse capital on order cancellation:', capitalErr.message, ); } } } // Prepare order data for email const emailOrderData = { orderNumber: updatedOrder._id.toString().slice(-8).toUpperCase(), total: updatedOrder.totalPrice, }; // Send status update email (don't wait for it) sendOrderStatusUpdateEmail( emailOrderData, updatedOrder.user, req.body.orderStatus, ).catch(() => { }); // Send notification based on status const userId = updatedOrder.user._id || updatedOrder.user; try { switch (req.body.orderStatus) { case 'created': await notifyOrderCreated(updatedOrder, userId); break; case 'processing': await notifyOrderProcessing(updatedOrder, userId); break; case 'shipped': await notifyOrderShipped(updatedOrder, userId); break; case 'completed': await notifyOrderCompleted(updatedOrder, userId); break; case 'cancelled': await notifyOrderCancelled(updatedOrder, userId); break; default: break; } } catch (notifErr) { // Silent fail } // ── Capital: Add profit when order is marked completed ── if (req.body.orderStatus === 'completed') { try { const populatedForProfit = await Order.findById( updatedOrder._id, ).populate({ path: 'items.product', select: 'costPrice purchasePrice', }); let orderProfit = 0; for (const item of populatedForProfit.items) { if (!item.product) continue; const costPrice = item.product.costPrice || 0; const sellingPrice = item.unitPrice; orderProfit += (sellingPrice - costPrice) * item.quantity; } if (orderProfit > 0) { const capital = await Capital.getCapital(); capital.currentCapital += orderProfit; capital.logs.push({ type: 'profit', amount: orderProfit, balanceAfter: capital.currentCapital, reference: updatedOrder._id, referenceModel: 'Order', description: `Profit from completed order #${updatedOrder._id.toString().slice(-8).toUpperCase()}`, date: new Date(), createdBy: req.user._id, }); await capital.save(); } } catch (capitalErr) { // Silent fail — don't block order update console.error( 'Failed to update capital on order completion:', capitalErr.message, ); } } // Explicitly broadcast order update for the dashboard (ONLY ONCE) emitOrderUpdate(updatedOrder, req.body.orderStatus); } res.status(200).json({ status: 'success', data: { order: updatedOrder } }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } }; exports.cancelOrder = async (req, res) => { try { const { id } = req.params; const order = await Order.findById(id); if (!order) return res .status(404) .json({ status: 'fail', message: 'Order not found' }); if (!req.user) return res.status(401).json({ status: 'fail', message: 'You must be logged in to cancel an order', }); const orderUserId = (order.user && order.user._id) || order.user; const isOwner = orderUserId && String(orderUserId) === String(req.user._id); const isAdmin = req.user.role === 'admin'; if (!isOwner && !isAdmin) return res.status(403).json({ status: 'fail', message: 'Not authorized to cancel this order', }); if (order.orderStatus === 'cancelled') { return res .status(400) .json({ status: 'fail', message: 'Order is already cancelled' }); } await restoreOrderStock(order.items); if (order.promo) { try { await Promo.findByIdAndUpdate(order.promo, { $inc: { usedCount: -1 } }); } catch (promoErr) { console.error('Failed to restore promo usage:', promoErr); } } order.orderStatus = 'cancelled'; order.canceledAt = new Date(); if (order.payment && order.payment.status !== 'failed') { order.payment.status = 'failed'; } await order.save(); // Send cancellation notification try { const userId = order.user; await notifyOrderCancelled(order, userId, 'Order cancelled as requested'); } catch (notifErr) { // Silent fail } // Explicitly broadcast order update for the dashboard (ONLY ONCE) emitOrderUpdate(order, 'order_cancelled'); // Populate product details before returning await order.populate('items.product', 'nameAr nameEn imageCover price'); res.status(200).json({ status: 'success', data: { order } }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } }; exports.deleteOrder = async (req, res) => { try { const order = await Order.findByIdAndDelete(req.params.id); if (!order) { return res.status(404).json({ status: 'fail', message: 'No order found', }); } // Explicitly broadcast order update for the dashboard emitOrderUpdate({ _id: req.params.id }, 'order_deleted'); res.status(204).json({ status: 'success', data: null, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message, }); } }; exports.getOrderSuccessPage = async (req, res) => { try { const { id } = req.params; const order = await Order.findById(id).populate('items.product').lean(); if (!order) return res .status(404) .json({ status: 'fail', message: 'Order not found' }); res.status(200).json({ status: 'success', data: { message: 'Order placed successfully', order, }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } }; function computeDiscountAmount(type, value, base) { const numericBase = Number(base) || 0; const numericValue = Number(value) || 0; if (type === 'shipping') return 0; // Does not affect subtotal base if (type === 'percentage') return Math.max(0, (numericBase * numericValue) / 100); return Math.max(0, numericValue); } const SiteSettings = require('../models/siteSettingsModel'); exports.createOrder = async (req, res) => { try { const { name, mobile, address, payment, items, promoCode, source } = req.body; // Check guest checkout settings if (!req.user) { const settings = await SiteSettings.getSettings(); if (!settings.checkout.allowGuestCheckout) { return res.status(401).json({ status: 'fail', message: 'Guest checkout is disabled. Please log in or create an account to place an order.', }); } } if (!items || !Array.isArray(items) || items.length === 0) { return res .status(400) .json({ status: 'fail', message: 'Cart items are required' }); } if (payment && payment.method === 'visa' && !req.user) { return res.status(400).json({ status: 'fail', message: 'You must be logged in to pay with Visa', }); } const productIds = items.map((i) => i.productId); const products = await Product.find({ _id: { $in: productIds } }); const productMap = new Map(products.map((p) => [String(p._id), p])); const missingIds = productIds.filter((id) => !productMap.has(String(id))); if (missingIds.length > 0) { return res.status(400).json({ status: 'fail', message: 'Some products were not found', missingProductIds: missingIds, }); } // Decrease stock for each product and track out-of-stock items // Decrease stock for each product in bulk const bulkOps = items.map((i) => ({ updateOne: { filter: { _id: i.productId }, update: { $inc: { stock: -(Number(i.quantity) || 1) } }, }, })); await Product.bulkWrite(bulkOps); // Identify products that went out of stock const outOfStockItems = await Product.find({ _id: { $in: productIds }, stock: { $lte: 0 }, }); let subtotal = 0; const orderItems = []; for (const i of items) { const prod = productMap.get(String(i.productId)); const quantity = Number(i.quantity) || 1; const unitPrice = prod.salePrice && prod.salePrice < prod.price ? prod.salePrice : prod.price; subtotal += unitPrice * quantity; orderItems.push({ product: prod._id, name: prod.nameAr || prod.nameEn || prod.barCode, quantity, unitPrice, }); } // Dynamic Shipping Calculation: // Strictly find price for the governorate level const govPrice = await ShippingPrice.findOne({ $or: [ { areaNameAr: address.governorate }, { areaNameEn: { $regex: new RegExp(`^${address.governorate.trim()}$`, 'i'), }, }, ], type: 'city', }); const SHIPPING_COST = govPrice ? govPrice.fees : 300; const deliveryDays = govPrice ? govPrice.deliveryDays : 3; // Calculate estimated delivery date const estimatedDeliveryDate = new Date(); estimatedDeliveryDate.setDate( estimatedDeliveryDate.getDate() + deliveryDays, ); let totalPrice = subtotal + SHIPPING_COST; let appliedPromo = null; let discountAmountApplied = 0; if (promoCode) { const upper = String(promoCode).trim().toUpperCase(); const now = new Date(); const promo = await Promo.findOne({ code: upper, active: true, $and: [ { $or: [{ startsAt: null }, { startsAt: { $lte: now } }] }, { $or: [{ expiresAt: null }, { expiresAt: { $gt: now } }] }, ], minOrderValue: { $lte: subtotal }, }); if (!promo) return res .status(400) .json({ status: 'fail', message: 'Invalid or expired promo code' }); if (promo.perUserLimit && promo.perUserLimit > 0) { if (!req.user) return res.status(400).json({ status: 'fail', message: 'You must be logged in to use this promo code', }); const userUses = await Order.countDocuments({ user: req.user._id, promo: promo._id, }); if (userUses >= promo.perUserLimit) return res.status(400).json({ status: 'fail', message: 'Promo code usage limit reached for this user', }); } if (promo.usageLimit != null && promo.usedCount >= promo.usageLimit) return res .status(400) .json({ status: 'fail', message: 'Promo code has been fully used' }); await Promo.findByIdAndUpdate(promo._id, { $inc: { usedCount: 1 } }); appliedPromo = promo; const discount = computeDiscountAmount(promo.type, promo.value, subtotal); discountAmountApplied = discount; if (promo.type === 'shipping') { totalPrice = subtotal; // Shipping is free discountAmountApplied = SHIPPING_COST; // Record the shipping saving as the discount } else { totalPrice = Math.max(0, subtotal - discount) + SHIPPING_COST; } } const normalizedPayment = { ...payment }; // Don't automatically mark visa payments as paid - wait for Paymob callback if (!normalizedPayment || !normalizedPayment.status) { normalizedPayment.status = 'pending'; } let promoData = {}; if (promoCode && appliedPromo) { const discountAmount = Number(discountAmountApplied.toFixed(2)); promoData = { promo: appliedPromo._id, promoCode: appliedPromo.code, discountAmount, discountType: appliedPromo.type, }; } const orderData = { name, mobile, address, items: orderItems, payment: normalizedPayment, totalPrice, estimatedDeliveryDate, shippingPrice: SHIPPING_COST, source: source || 'direct', ...promoData, }; if (req.user) orderData.user = req.user._id; let newOrder; try { newOrder = await Order.create(orderData); } catch (err) { // Rollback: Restore stock if order creation fails try { await restoreOrderStock(orderItems); } catch (restoreErr) { console.error( 'Failed to restore stock after order creation error:', restoreErr, ); } if (appliedPromo) { try { await Promo.findByIdAndUpdate(appliedPromo._id, { $inc: { usedCount: -1 }, }); } catch (err2) { // Silent fail or use a proper logger } } throw err; } // Send notifications (ONLY for CASH orders) // For Visa, we notify only after successful payment confirmation in paymentController if (newOrder.payment.method === 'cash') { try { if (req.user) { notifyOrderCreated(newOrder, req.user._id).catch(() => { }); } const vendorIds = new Set(); products.forEach((p) => { if (p.provider) vendorIds.add(p.provider.toString()); }); vendorIds.forEach((vId) => { notifyVendorNewOrder(newOrder, vId).catch(() => { }); }); notifyAdminsNewOrder(newOrder).catch(() => { }); } catch (notifErr) { // Silent fail } } // Out-of-stock notifications should happen regardless of payment method // because stock IS deducted from the DB now try { outOfStockItems.forEach((prod) => { if (prod.provider) { notifyProductOutOfStock(prod, prod.provider).catch(() => { }); } notifyAdminsProductOutOfStock(prod).catch(() => { }); }); } catch (vendorNotifErr) { // Silent fail } // Explicitly broadcast order update for the dashboard (ONLY ONCE) emitOrderUpdate(newOrder, 'order_created'); res.status(201).json({ status: 'success', data: { order: newOrder }, }); // Append to Google Sheet (fire-and-forget — never blocks the response) appendOrderToSheet(newOrder).catch(() => { }); // Send order confirmation email (non-blocking - after response) // ONLY for cash orders. Visa orders will send once payment is confirmed. if (req.user && newOrder.payment.method === 'cash') { // Populate order with product details and provider for email const populatedOrder = await Order.findById(newOrder._id).populate({ path: 'items.product', select: 'nameAr price imageCover', populate: { path: 'provider', select: 'storeName', }, }); // Prepare order data for email template const emailOrderData = { orderNumber: newOrder._id.toString().slice(-8).toUpperCase(), // Last 8 chars of ID items: populatedOrder.items.map((item) => ({ product: { nameAr: item.product && item.product.nameAr ? item.product.nameAr : item.name, imageCover: item.product && item.product.imageCover ? item.product.imageCover : null, provider: item.product && item.product.provider ? item.product.provider : null, }, quantity: item.quantity, price: item.unitPrice, })), subtotal: subtotal, discount: discountAmountApplied, shippingCost: SHIPPING_COST, total: totalPrice, paymentMethod: newOrder.payment.method, shippingAddress: { street: newOrder.address.street, city: newOrder.address.city, governorate: newOrder.address.governorate, phone: newOrder.mobile, }, }; sendOrderConfirmationEmail(emailOrderData, req.user).catch(() => { }); } // Notify all admin users by email about the new order User.find({ role: 'admin' }) .select('email name') .lean() .then((admins) => { if (!admins || admins.length === 0) return; // Reuse emailOrderData if already built (cash orders), otherwise build a minimal version const adminOrderData = { orderNumber: newOrder._id.toString().slice(-8).toUpperCase(), items: newOrder.items.map((item) => ({ product: { nameAr: item.name, imageCover: null, provider: null }, quantity: item.quantity, price: item.unitPrice, })), subtotal, discount: discountAmountApplied, shippingCost: SHIPPING_COST, total: totalPrice, paymentMethod: newOrder.payment.method, shippingAddress: { street: newOrder.address.street, city: newOrder.address.city, governorate: newOrder.address.governorate, phone: newOrder.mobile, }, }; const customer = req.user ? { name: req.user.name, email: req.user.email } : { name: 'عميل غير مسجل', email: '-' }; admins.forEach((admin) => { sendAdminNewOrderEmail(adminOrderData, customer, admin.email).catch( () => { }, ); }); }) .catch(() => { }); } catch (err) { res.status(500).json({ status: 'error', message: err.message, }); } }; exports.updateOrderItemFulfillment = async (req, res) => { try { const { orderId, productId } = req.params; const { fulfillmentStatus } = req.body; // Validate fulfillment status const validStatuses = ['pending', 'preparing', 'ready', 'cancelled']; if (!validStatuses.includes(fulfillmentStatus)) { return res .status(400) .json({ status: 'fail', message: 'Invalid fulfillment status' }); } const order = await Order.findById(orderId).populate('items.product'); if (!order) { return res .status(404) .json({ status: 'fail', message: 'Order not found' }); } // Find the item const item = order.items.find( (it) => it.product && it.product._id.toString() === productId, ); if (!item) { return res .status(404) .json({ status: 'fail', message: 'Product not found in this order' }); } // Authorization check // Admins and employees with manage_orders can update anything const isAdmin = req.user.role === 'admin'; const isEmployee = req.user.role === 'employee' && req.user.permissions.includes(PERMISSIONS.MANAGE_ORDERS); // For vendors, check if they own the product if (!isAdmin && !isEmployee) { if (req.user.role !== 'vendor' || !req.user.provider) { return res .status(403) .json({ status: 'fail', message: 'Not authorized' }); } const providerId = req.user.provider._id || req.user.provider; if (item.product.provider.toString() !== providerId.toString()) { return res.status(403).json({ status: 'fail', message: 'You can only update your own items', }); } } item.fulfillmentStatus = fulfillmentStatus; // Handle cancellation if (fulfillmentStatus === 'cancelled') { order.orderStatus = 'cancelled'; // Restore stock for all items in the order await restoreOrderStock(order.items); } else if ( // Auto-revert order status to processing if an item is moved back from 'ready' // when the order was already shipped or completed fulfillmentStatus !== 'ready' && ['shipped', 'completed'].includes(order.orderStatus) ) { order.orderStatus = 'processing'; } await order.save(); // Broadcast update emitOrderUpdate(order, 'order_item_updated'); res.status(200).json({ status: 'success', data: { order }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } }; exports.updateOrderItemQuantity = async (req, res) => { try { const { orderId, productId } = req.params; const { quantity } = req.body; if (!quantity || quantity < 1) { return res.status(400).json({ status: 'fail', message: 'Quantity must be at least 1', }); } const order = await Order.findById(orderId).populate('items.product'); if (!order) { return res.status(404).json({ status: 'fail', message: 'Order not found', }); } // Authorization check: Only admins or authorized employees can edit quantities const isAdmin = req.user.role === 'admin'; const isEmployee = req.user.role === 'employee' && req.user.permissions.includes(PERMISSIONS.MANAGE_ORDERS); if (!isAdmin && !isEmployee) { return res.status(403).json({ status: 'fail', message: 'Not authorized to change quantities', }); } // Find the item const item = order.items.find( (it) => it.product && it.product._id.toString() === productId, ); if (!item) { return res.status(404).json({ status: 'fail', message: 'Product not found in this order', }); } const oldQuantity = item.quantity; const quantityDiff = quantity - oldQuantity; // Update product stock if (quantityDiff !== 0) { const product = await Product.findById(productId); if (product) { // If increasing quantity, check if there's enough stock if (quantityDiff > 0 && product.stock < quantityDiff) { return res.status(400).json({ status: 'fail', message: `Insufficient stock for ${product.nameAr || product.nameEn}. Available: ${product.stock}`, }); } product.stock -= quantityDiff; await product.save(); } } // Update item quantity item.quantity = quantity; // Recalculate totals let newSubtotal = 0; order.items.forEach((it) => { newSubtotal += it.unitPrice * it.quantity; }); // Handle discount recalculation if there was a promo code if (order.promo) { const promo = await Promo.findById(order.promo); if (promo) { const newDiscount = computeDiscountAmount( promo.type, promo.value, newSubtotal, ); order.discountAmount = newDiscount; if (promo.type === 'shipping') { order.totalPrice = newSubtotal; } else { order.totalPrice = Math.max(0, newSubtotal - newDiscount) + order.shippingPrice; } } else { // Promo not found, keep old shipping logic but update total order.totalPrice = newSubtotal + order.shippingPrice - order.discountAmount; } } else { order.totalPrice = newSubtotal + order.shippingPrice - order.discountAmount; } await order.save(); // Broadcast update emitOrderUpdate(order, 'order_quantity_updated'); res.status(200).json({ status: 'success', data: { order }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } }; exports.addOrderItem = async (req, res) => { try { const { id: orderId } = req.params; const { productId, quantity } = req.body; if (!quantity || quantity < 1) { return res.status(400).json({ status: 'fail', message: 'Quantity must be at least 1', }); } const order = await Order.findById(orderId).populate('items.product'); if (!order) { return res.status(404).json({ status: 'fail', message: 'Order not found', }); } const product = await Product.findById(productId); if (!product) { return res.status(404).json({ status: 'fail', message: 'Product not found', }); } if (product.stock < quantity) { return res.status(400).json({ status: 'fail', message: `Insufficient stock for ${product.nameAr || product.nameEn}. Available: ${product.stock}`, }); } // Authorization check const isAdmin = req.user.role === 'admin'; const isEmployee = req.user.role === 'employee' && req.user.permissions.includes(PERMISSIONS.MANAGE_ORDERS); if (!isAdmin && !isEmployee) { return res.status(403).json({ status: 'fail', message: 'Not authorized to add items to orders', }); } // Check if product already exists in order const existingItem = order.items.find( (item) => item.product && item.product._id.toString() === productId, ); if (existingItem) { existingItem.quantity += quantity; existingItem.fulfillmentStatus = 'pending'; } else { order.items.push({ product: productId, name: product.nameAr || product.nameEn, quantity, unitPrice: product.salePrice && product.salePrice < product.price ? product.salePrice : product.price, }); } // Deduct stock product.stock -= quantity; await product.save(); // Recalculate totals let newSubtotal = 0; order.items.forEach((it) => { newSubtotal += it.unitPrice * it.quantity; }); if (order.promo) { const promo = await Promo.findById(order.promo); if (promo) { const newDiscount = computeDiscountAmount( promo.type, promo.value, newSubtotal, ); order.discountAmount = newDiscount; if (promo.type === 'shipping') { order.totalPrice = newSubtotal; } else { order.totalPrice = Math.max(0, newSubtotal - newDiscount) + order.shippingPrice; } } else { order.totalPrice = newSubtotal + order.shippingPrice - order.discountAmount; } } else { order.totalPrice = newSubtotal + order.shippingPrice - order.discountAmount; } await order.save(); // Broadcast update emitOrderUpdate(order, 'order_item_added'); res.status(200).json({ status: 'success', data: { order }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } }; exports.removeOrderItem = async (req, res) => { try { const { orderId, productId } = req.params; const order = await Order.findById(orderId); if (!order) { return res.status(404).json({ status: 'fail', message: 'Order not found', }); } // Authorization check const isAdmin = req.user.role === 'admin'; const isEmployee = req.user.role === 'employee' && req.user.permissions.includes(PERMISSIONS.MANAGE_ORDERS); if (!isAdmin && !isEmployee) { return res.status(403).json({ status: 'fail', message: 'Not authorized to remove items from orders', }); } // Find the item index const itemIndex = order.items.findIndex( (it) => it.product && it.product.toString() === productId, ); if (itemIndex === -1) { return res.status(404).json({ status: 'fail', message: 'Product not found in this order', }); } // Prevent removing the last item (order must have at least one item) if (order.items.length <= 1) { return res.status(400).json({ status: 'fail', message: 'Cannot remove the last item. Use "Delete Order" instead.', }); } const itemToRemove = order.items[itemIndex]; // Restore stock const product = await Product.findById(productId); if (product) { product.stock += itemToRemove.quantity; await product.save(); } // Remove the item order.items.splice(itemIndex, 1); // Recalculate totals let newSubtotal = 0; order.items.forEach((it) => { newSubtotal += it.unitPrice * it.quantity; }); if (order.promo) { const promo = await Promo.findById(order.promo); if (promo) { const newDiscount = computeDiscountAmount( promo.type, promo.value, newSubtotal, ); order.discountAmount = newDiscount; if (promo.type === 'shipping') { order.totalPrice = newSubtotal; } else { order.totalPrice = Math.max(0, newSubtotal - newDiscount) + order.shippingPrice; } } else { order.totalPrice = newSubtotal + order.shippingPrice - order.discountAmount; } } else { order.totalPrice = newSubtotal + order.shippingPrice - order.discountAmount; } await order.save(); // Broadcast update emitOrderUpdate(order, 'order_item_removed'); res.status(200).json({ status: 'success', data: { order }, }); } catch (err) { res.status(500).json({ status: 'error', message: err.message }); } };